JVM - 类加载器(二)

1,312 阅读3分钟

白蛇缘起0_1920X804.jpg

前言

JDK9 之后,使用了模块化的概念,与 JDK8 及之前的类加载器 相比,也有了一些变化。

类加载器

加载器类别的变化

类加载器的类别上,用 平台类PlatformClassLoader 代替了扩展类加载器,引导类加载器和系统类加载器保持不变。

引导类加载器是由类库和代码在虚拟机中实现的。 为了向后兼容,它在程序中仍然使用 null 表示。

// jdk11 test
// Example 01: 三种类加载
public class ClassLoaderDemo{
    public static void main(String[] args){
        String str = new String("hello bootstrap classloader");
        System.out.println(str.getClass().getClassLoader()); // null

        Date date = new Date(System.currentTimeMillis()); 
        // jdk.internal.loader.ClassLoaders$PlatformClassLoader@6cd8737
        System.out.println(date.getClass().getClassLoader()); 

        // // jdk.internal.loader.ClassLoaders$AppClassLoader@2437c6dc
        System.out.println(ClassLoaderDemo.class.getClassLoader());
    }
}

如上所示,从输出的结果看,String 类是由 ApplicationClassLoader 加载的, Date 类是由 PlatformClassLoader 加载的,而我们自定义的类依然是由 AppClassLoader 加载的。

加载路径的变化

模块化将源码分为了很多小的模块,每个模块都有一共类加载器负责对应加载其中的类。

  • BootClassLoader 加载的模块
java.base            java.security.sasl
java.datatransfer    java.xml
java.desktop         jdk.httpserver
java.instrument      jdk.internal.vm.ci
java.logging         jdk.management
java.management      jdk.management.agent
java.management.rmi  jdk.naming.rmi
java.naming          jdk.net
java.prefs           jdk.sctp
java.rmi             jdk.unsupported
  • PlatformClassLoader 加载的模块
java.activation    java.xml.ws.annotation  jdk.desktop         java.compiler
javafx.base        jdk.dynalink            java.corba          javafx.controls
jdk.javaws         java.jnlp               javafx.deploy       jdk.jsobject
java.scripting     javafx.fxml             jdk.localedata      java.se
javafx.graphics    jdk.naming.dns          java.se.ee          javafx.media
jdk.plugin         java.security.jgss      javafx.swing        jdk.plugin.dom
java.smartcardio   javafx.web              jdk.plugin.server   java.sql
jdk.accessibility  jdk.scripting.nashorn   java.sql.rowset     jdk.charsets
jdk.security.auth  java.transaction        jdk.crypto.cryptoki jdk.security.jgss 
java.xml.bind      jdk.crypto.ec           jdk.xml.dom         java.xml.crypto
jdk.crypto.mscapi  jdk.zipfs               java.xml.ws         jdk.deploy
  • AppClassLoader 加载的模块
jdk.aot               jdk.jdeps
jdk.attach            jdk.jdi
jdk.compiler          jdk.jdwp.agent
jdk.editpad           jdk.jlink
jdk.hotspot.agent     jdk.jshell
jdk.internal.ed       jdk.jstatd
jdk.internal.jvmstat  jdk.pack
jdk.internal.le       jdk.policytool
jdk.internal.opt      jdk.rmic
jdk.jartool           jdk.scripting.nashorn.shell
jdk.javadoc           jdk.xml.bind*
jdk.jcmd              jdk.xml.ws*
jdk.jconsole

注意,上面的 类加载 > 加载器类别的变化 中的 Example 01 的例子中,Date 类是 java.sql 下的 Date,所以由 PlatformClassLoader 加载。

继承结构的变化

JDK9 之前,扩展类加载器和系统类加载器都继承自 URLClassLoader,我们之前也提到,如果要更加方便地自定义类加载器,可以直接继承自 URLClassLoader

JDK9 之后,移除了 URLClassLoader,引导类加载器有了具体的实现 BootClassLoader,并且三种类加载器都继承自 BulitinClassLoader

image.png

使用类加载器的变化

我们可以使用类加载器加载一个类,并获取它的实例。之前我们可以使用 newInstance() 方法获取对象实例。

JDK9 开始,已经将 ClassLoadernewInstance 方法标为过时,并且建议使用如下方式代替:

clazz.getDeclaredConstructor().newInstance()

原来的方法必须保证类有一共空构造方法,而新的方式通过反射的形式,可以适用于任何情况。

public class ClassLoaderDemo03 {

    // 只有一共有参构造函数,没有空构造函数
    public ClassLoaderDemo03(String info){
        System.out.println(info);
    }

    public static void main(String[] args) {
        ClassLoader loader = Thread.currentThread().getContextClassLoader();
        // <= jdk8
        try {
            // 异常
            Object obj01 = (ClassLoaderDemo03) loader.loadClass("com.zcat.jvm.classloader.ClassLoaderDemo03").newInstance();
        } catch (Exception e) {
            e.printStackTrace();
        }

        // >= jdk9
        try {
            // 成功创建实例
            Object obj01 = (ClassLoaderDemo03) loader.loadClass("com.zcat.jvm.classloader.ClassLoaderDemo03")
                    .getDeclaredConstructor(String.class)
                    .newInstance("使用 >=jdk9 构造的对象");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

输出的结果如下:

java.lang.InstantiationException: com.zcat.jvm.classloader.ClassLoaderDemo03
使用 >=jdk9 构造的对象

双亲委派模式的变化

双亲委派模式的基本结构依然保持,但是类加载器之间的委派关系略有变化。

由于每个类都属于一个模块,所以当收到加载类请求的后,如果能够找到该类属于的模块,就先委派给所属模块的类加载器加载;如果没有找到这样的模块,才会先委派给父类加载器加载。

我们可以在 BuiltinClassLoaderloadClassOrNull() 方法中看到具体的加载逻辑:

protected Class<?> loadClassOrNull(String cn, boolean resolve) {
    synchronized (getClassLoadingLock(cn)) {
        // 是否已经被加载过
        Class<?> c = findLoadedClass(cn);
        if (c == null) {
            // 未被加载过,首先找到对应的模块
            LoadedModule loadedModule = findLoadedModule(cn);
            if (loadedModule != null) {
                // 能找到对应的模块,委派给负责该模块的类加载器
                BuiltinClassLoader loader = loadedModule.loader();
                if (loader == this) {
                    if (VM.isModuleSystemInited()) {
                        c = findClassInModuleOrNull(loadedModule, cn);
                    }
                } else {
                    c = loader.loadClassOrNull(cn);
                }
            } else {
                // 没有找到模块,委派给父类加载器
                if (parent != null) {
                    c = parent.loadClassOrNull(cn);
                }
                if (c == null && hasClassPath() && VM.isModuleSystemInited()) 
                    c = findClassOnClassPathOrNull(cn);
                }
            }
        }
        if (resolve && c != null) resolveClass(c);
        return c;
    }
}

这中变化也打破了双亲委派,如果要加载的类在 BootClassLoader 负责的模块中,则会跳过 BuiltinClassLoader ,直接委派给 BootClassLoader 加载。