最近在学习 Tomcat 的内部知识,了解到 Tomcat 也打破了双亲委派模型,想到之前 springBoot 的启动流程也是通过 SPI 机制破坏了双亲委派模型,因此觉得有必要总结一下类加载机制的原理。
讲类的加载机制之前,有必要了解一下类的生命周期,包括:加载,验证,准备,解析,初始化,使用,卸载这 7 个阶段。
加载阶段:方式包括:通过一个类的全限定名,可以从本地 jia 包,resource、网络 URL 等途径加载。加载后,会在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的访问入口。
链接阶段:
初始化阶段:对类的静态变量,静态代码块执行初始化操作。初始化时机:使用 new,调用类的静态方法,读取类的静态字段,反射
卸载阶段:虚拟机退出,程序结束
先来概括一下,什么是类的加载器,他是一个通过全限类定名获取一个类的二进制数据流,并将该流转换成对应类的一套代码或程序。
类的加载器一般我们分为引导类加载器(Bootstrap ClassLoader)和自定义类加载器(User-Defined ClassLoader)。这里的自定义类加载器其实包括扩展类加载器(Extension ClassLoader)和系统类加载器(AppClassLoader)。
Bootstrap 是由虚拟机负责的加载底层由 c、c++代码实现的核心库。如<JAVA_HOME>\lib 目录;
Extension,加载对象是
Application,也叫 System ClassLoader,加载用户类路径 classpath 的类库。
双亲委派模型 && 缓存机制
类的加载机制是一层层向上委托加载的(先找父加载器加载),即按照 AppClassLoader->Extension ClassLoader->Bootstrap ClassLoader 的委托形式,只有当根类 Bootstrap ClassLoader 无法加载时,才会自己尝试去加载,否则抛出 ClassNotFindException。另外,一个类中引用的类也会遵循该类的加载器机制。
这种机制的优势是显而易见的:
与双亲委派机制相对应的就是全盘委托机制,即一个类自始至终都为一个 ClassLoader 负责,除非显示使用其他的 ClassLoader。
类加载的方式:(获取 Class 类对象的方式)
API:
1,不破坏双亲模型,只需继承 ClassLoader,重写 findClass 方法即可。
双亲委派的逻辑主要是在 loadclass 方法,然后检查这个类是否会被加载,没有加载则找父类加载器,递归后父类加载器没有加载,再自己调用 findClass 方法加载这个类。很明显,我们只要重写这个方法就好。
/**
* 被加载类,初始化时输出类的加载其
*/
public class Test {
public Test(){
System.out.println(this.getClass().getClassLoader().toString());
}
}
package me.lsk.test;
import java.io.*;
/**
* 自定义不打破双亲机制的类加载器,只需继承 ClassLoader 然后重写 findClass 方法,改变加载顺序即可。
* 先加载 Test.class
*/
public class MyClassLoader extends ClassLoader {
private String name;
public MyClassLoader(ClassLoader parent, String name){
super(parent);
this.name = name;
}
@Override
public String toString(){
return this.name;
}
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
MyClassLoader loader = new MyClassLoader(MyClassLoader.class.getClassLoader(), "MyClassLoader");
Class aClass = loader.loadClass("me.lsk.test.Test");
// newInstance 根据无参构造生成对象
Object object = aClass.newInstance();
// 会执行构造方法
}
/**
* 获得类的 Class 对象
* @param name
* @return
* @throws ClassNotFoundException
*/
@Override
public Class<?> findClass(String name){
InputStream in = null;
byte[] data = null;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
in = new FileInputStream(new File("D:/Test.class"));
int c = 0;
while(-1 != (c=in.read())){
baos.write(c);
}
data = baos.toByteArray();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
in.close();
baos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
// 输出指定名称的 class
return this.defineClass(name,data,0,data.length);
}
}
// 输出
sun.misc.Launcher$AppClassLoader@18b4aac2
2,破坏双亲模型,继承 ClassLoader 后重写 loadClass 和 findClass 方法。
loadClass 决定了先加载父类的逻辑,重写他就可以打破双亲委派逻辑。
// 在 MyClassLoader 里补充以下逻辑:
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
ClassLoader loader = getSystemClassLoader();
Class<?> aClass = null;
aClass= loader.loadClass(name);
if(aClass!=null) return aClass;
// 加载不到再由 findclass 走双亲机制
aClass = findClass(name);
return aClass;
}
SPI 机制
什么是 SPI(Service Provider Interface)?它是 JDK 内置的一种服务提供发现机制,通过 JDK 提供接口,第三方实现和扩展 API。动态替换发现的能力,实现了接口和实现的解耦。
如何使用?我们在 META-INF/services 下写入要加载的类的全限定文件名,然后就可以使用 serviceloder 工具类加载,即可获得一个迭代器集合。原理?通过 ContextClassLoader 线程上下文类加载器,通过改变加载顺序,破坏双亲委派机制,解决了第三方类库的加载问题。
JNDI 机制
什么是 JNDI(Java Naming and Directory Interface)?JAVA 命名和目录接口,使得我们可以通过路径名方便的访问资源。通过 JNDI,把一个 Java 对象和一个特定的名称关联在一起,方便容器后续使用,实现了解耦。
Tomcat 为什么要破坏双亲机制?
每个 Tomcat 需要支持部署多个应用,不同应用可能依赖同一类库,也可能依赖不同类库,也可能依赖不通类库的不同版本。这也就意味着 web 应用资源间既需要共享也需要隔离。如果依然遵从 JDK 的双亲委派机制,那么整个系统只能缓存一份类库,这显然是不能支持不同应用依赖不同类库的需求。所以 Tomcat 需要打破该机制。
Tomcat 是如何做的?
Tomcat 破坏双亲委派,通过提供隔离的机制,为每个 web 容器单独提供一个 WebAppClassLoader 加载器。每个应用先使用自己的类加载器 WebAppClassLoader 负责加载本身的目录下的 class 文件,加载不到时再交给公共资源加载器 CommonClassLoader 加载,他再按照向上加载的原则,交给系统类加载器、扩展类加载器、启动类加载器加载。
这和双亲委派刚好相反。这样做也使得 Tomcat 具备热插拔功能。
在 springboot 的自动装配过程中,最终会加载 META-INF/spring.factories 文件,而加载的过程是由 SpringFactoriesLoader 加载的。从 CLASSPATH 下的每个 Jar 包中搜寻所有 META-INF/spring.factories 配置文件,然后将解析 properties 文件,找到指定名称的配置后返回。
JDK 面向不同的数据服务商提供了统一的驱动接口,然后我们的数据库厂商就可以自定义驱动实现接口,从而创建连接,如 JDBC,ODBC 等方式。这里的 DriverManager 会由启动类加载器加载,但是里面调用的 Drier 类的,是第三方实现的,只能由系统类加载器加载 classpath 获得。然而,启动类加载器是不能向下委托加载的,所以这里就需要破坏这种双亲委派机制了。
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mysql", "root", "123456");
JDBC 通过 SPI 机制,利用上下文加载器,获得当前的加载器 AppClassLoader 实现驱动加载,这里的驱动在META-INF/services/java.sql.Driver
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
....
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
这一节,我们从 JVM 的类加载机制聊起,包括类的生命周期、自定义类的加载器,以及双亲委派机制。
同时讨论了双亲委派这样做的好处?单向加载带来的弊端。一些框架中破坏双亲委派的典型应用等。
https://juejin.cn/post/6865572557329072141
https://blog.csdn.net/u010841296/article/details/89731566
https://www.cnblogs.com/hollischuang/p/14260801.html