提到 ClassLoader,最先想到的一定是“双亲委派”了,加载类时优先使用父类加载器(parent classloader),不过除了这个委托模型之外,还有很多细节值得研究
加载时机
除了显示调用ClassLoader.loadClass 进行加载Class之外,JVM在下面的5种场景下,也会执行加载Class的操作(由JVM调用ClassLoader.loadClassInternal)
- 使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
- 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
- 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
- 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
- 当使用JDK 1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
以上几种加载时机,统称为主动引用的方式;除此之外,其他引用类的方式都不会被触发Class的加载
比如下面这种情况,就不属于主动引用,不会进行类的加载
public class NotInitialization {
public static void main(String[] args) {
//根据场景1,读取的是常量,不会造成类的初始化
System.out.println(ConstClass.HELLOWORLD);
}
}
class ConstClass{
static final String HELLOWORLD = "hello world";
static {
System.out.println("ConstClass init!");
}
}
Class.forName
通过 Class.forName 的形式,可以进行进行类的加载,不过这里使用的是调用者(caller)的类加载器,也就是发起Class.forName 方法调用的类的类加载器
public static Class> forName(String className)
throws ClassNotFoundException {
//获取caller Class
Class> caller = Reflection.getCallerClass();
return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}
类加载器 的“传递性”
首先说一下两个概念,定义加载器(defining loader)和初始加载器(initiating loader)
现有一个ClassLoader A,如果通过A直接定义(而不是在A内委托parent classLoader加载)了一个Class X,那么称ClassLoader A是Class X的定义加载器,也称ClassLoader A定义了Class X
**
当一个ClassLoader委托parent classLoader进行加载某类(loadClass),那么此时loadClass的classLoader和实际上defineClass的parent classLoader其实并不是同一个
现有一个ClassLoader B,通过ClassLoader B的loadClass方法加载Class Y,无论ClassLoader B是直接define了Class Y,还是委托parent classLoader 去define了Class Y,那么ClassLoader B都是Class Y的initiating loader。只不过如果Class Y是由ClassLoader B直接加载的,那么Class Y的定义加载器和初始加载器 都是ClassLoader B;如果是委托父类parent classLoader C加载的,那么定义加载器就是ClassLoader C,而初始加载器就是ClassLoader B
如下图所示,对于被加载的Class X来说,ClassLoader A就是定义加载器,而实际上ClassLoader A是委托了ClassLoader B去完成define的,所以ClassLoader B是定义加载器
使用Class.forName,这种形式加载的Class,实际上也是使用调用者(caller)的定义加载器
对于上面提到的“主动引用”方式的加载Class,在加载机制上有一些细节需要注意:
- JVM在解释Class时实际上是“惰性加载”的,在解释执行的行遇到引用后才会解析引用的Class;比如在方法体中调用了某类,只有在执行到这一行时才会进行加载
- 对于同一个ClassLoader实例来说,在当前Class中没有加载过的Class,会使用发起引用类的定义加载器(而不是定义加载器)进行加载
- 如果一个ClassLoader中已经加载过某个Class,那么就不会再加载(连loadClass都不会调用);比如Class A里引用了Class X和Class B,Class B里也引用了Class X,那么在执行Class B时,不会再发生loadClass(X)的操作
根据上面几个特点,自定义ClassLoader就比较简单了,比如Spring Boot 提供的可执行Jar(Executable Jar)的ClassLoader实现中:
只需要创建一个负责加载jar包内jar包的ClassLoader(org.springframework.boot.loader.JarLauncher),然后入口类中通过该ClassLoader去加载我们代码中的Main-Class即可,这样就可以做到加载jar包中的jar包了:
public void run() throws Exception {
//通过前面设置的“负载加载jar包内jar包的ClassLoader”,去加载我们程序中的main类
Class> mainClass = Thread.currentThread().getContextClassLoader()
.loadClass(this.mainClassName);
Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
mainMethod.invoke(null, new Object[] { this.args });
}
参考
- https://blogs.oracle.com/sundararajan/understanding-java-class-loading
- https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-5.html
- 《深入理解Java虚拟机: JVM 高级特性与最佳实践(第2版)》 - 周志明 著