关于java:ClassLoader-中可能被忽视的细节

5次阅读

共计 2940 个字符,预计需要花费 8 分钟才能阅读完成。

提到 ClassLoader,最先想到的肯定是“双亲委派”了,加载类时优先应用父类加载器(parent classloader),不过除了这个委托模型之外,还有很多细节值得钻研

加载机会

除了显示调用 ClassLoader.loadClass 进行加载 Class 之外,JVM 在上面的 5 种场景下,也会执行加载 Class 的操作(由 JVM 调用 ClassLoader.loadClassInternal)

  1. 应用 new 关键字实例化对象的时候、读取或设置一个类的动态字段(被 final 润饰、已在编译期把后果放入常量池的动态字段除外)的时候,以及调用一个类的静态方法的时候。
  2. 应用 java.lang.reflect 包的办法对类进行反射调用的时候,如果类没有进行过初始化,则须要先触发其初始化。
  3. 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则须要先触发其父类的初始化。
  4. 当虚拟机启动时,用户须要指定一个要执行的主类(蕴含 main()办法的那个类),虚构机会先初始化这个主类。
  5. 当应用 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 版)》– 周志明 著
正文完
 0