Skip to content

类加载机制

思维导图

类的生命周期

​ 类的生命周期描述了一个类从创建到销毁的整个过程。总体而言,类的生命周期可以分为加载连接初始化使用卸载 5 个阶段。其中,连接阶段还可以进一步细分为验证准备解析 3 个阶段,因此有时类的生命周期也被视为 7 个阶段。

类的生命周期

​ 类的加载、连接和初始化这 3 个阶段描述了类在内存中的创建过程;类的使用阶段描述了类在应用程序中被访问和运用的过程,包括创建对象、调用方法和访问字段(属性)等操作;而类的卸载阶段描述了类在内存中被销毁的过程。

​ 类的使用阶段主要面向开发者,而卸载阶段与垃圾回收密切相关(有关类卸载的详细内容将在垃圾回收章节中讨论)。本章节将重点讨论类在内存中的创建过程,即类的加载、连接和初始化这 3 个阶段。由于这一过程始于加载阶段,因此创建过程通常也被称为类加载机制

加载

​ 类的加载阶段由类加载器负责。首先,类加载器根据类的全限定名获取相应字节码的二进制流;接着,将这些二进制数据转换为JVM可识别的内部数据结构;最后,将这些数据结构存储在运行时数据区,即JVM管理的内存中。

加载

​ 类加载器可以通过多种渠道获取字节码的二进制流,例如本地磁盘文件、运行时动态代理或网络传输等。不论采用哪种方式,类加载器始终关注的重点是字节码二进制数据是否能够成功转换为JVM可识别的内部数据结构。

类加载器获取字节码二进制流的渠道

​ 在加载阶段,字节码的二进制流转换后将以什么样的数据结构存储在JVM的运行时数据区呢?这与具体的虚拟机实现密切相关,但本质上,它们存储的数据都是为了描述字节码所表示的类信息,即Java语言中的Class概念。

​ 以HotSpot虚拟机为例,它会在方法区中创建一个InstanceKlass对象,该对象用于存储类的元信息和额外信息。此外,HotSpot还会在堆中创建一个与方法区中的InstanceKlass对象相互关联的java.lang.Class对象,该Class对象提供了许多本地native方法,以便开发者获取特定的类信息。需要注意的是,方法区中的类对象属于C++层面,它们在Java层面是不可访问的,而堆中的类对象则属于Java层面,这种设计有效地控制了开发者访问类相关信息的范围,保证了系统的安全性和稳定性。

InstanceKlass对象和Class对象

连接

​ 类的连接阶段可以进一步细分为验证准备解析 3 个子阶段。


​ 验证阶段的主要任务是检查字节码数据的各个部分是否符合《Java虚拟机规范》中的规定。虽然这一阶段在整个类加载过程中消耗的系统资源相对较多,但它能够确保后续程序的安全运行。验证阶段主要包括以下四个步骤。

  1. 文件格式验证:校验字节码是否符合Class文件格式规范。例如文件是否以0xCAFEBABE开头,主次版本号是否满足当前Java虚拟机要求等。
  2. 元信息验证:对字节码的描述信息进行语义分析,校验其是否遵循《Java虚拟机规范》。例如类是否拥有父类(除Object外的类都必须拥有父类),类是否继承了用final修饰的类等。
  3. 字节码指令验证:校验方法的字节码指令的语义是否合法且符合逻辑。例如指令跳转的位置是否正确,对象类型的转换是否合理等。
  4. 符号引用验证:验证符号引用是否合法。例如类依赖的其他类、方法、字段的符号引用是否存在,类是否按照正确的权限访问符号引用等。

​ 准备阶段的主要任务是为类的静态变量分配内存并为其设置初始值。在Java中,各种数据类型都有其对应的初始值,如下表所示。

各数据类型的初始值

case

​ 以下是一个代码示例,Foo类中定义了一个静态变量unit。在准备阶段,unit静态变量会在内存中被分配,并且其初始值为 0

类准备示例代码

​ 值得说明的是,当一个静态变量的类型为简单数据类型(如基本数据类型或String类型)且被final关键字修饰时,它会在类的准备阶段直接被赋值为常量。

case

​ 以下是一个代码示例,Foo类中定义了一个静态变量unit。在准备阶段,unit静态变量会在内存中被分配,并且其初始值为 1024

准备阶段常量示例代码

​ 为了验证这一结论,我们可以使用Jclasslib工具查看Foo类的字节码信息,如下图所示。可以看到,unit字段具有ConstantValue信息,这表明该字段是一个常量,值在编译期就已确定。因此,在类的准备阶段,unit在内存中的初始值为 1024

准备阶段常量示例Jclasslib

​ 那么开发者设置的静态变量(非常量)的值是在什么阶段赋值的呢?答案是初始化阶段,相关内容将会在下文中详细讲解。

TIP

​ 在JDK1.8之前,HotSpot虚拟机将静态变量存储在方法区中的Klass对象中;而从JDK1.8及以后,静态变量则转移到了堆中的Class对象之内。


​ 解析阶段的主要任务是将常量池中的符号引用转换为直接引用。以下是符号引用和直接引用的详细解释。

  • 符号引用Symbolic References):符号引用通过一组符号来描述所引用的目标,这些符号可以是任何形式的字面量,只要在使用时能够明确且无歧义地定位到目标即可。符号引用的特点在于其与虚拟机的内存布局无关,但不同的虚拟机实现必须接受相同的符号引用,这是因为符号引用的字面量形式在《Java虚拟机规范》中有明确的定义。

  • 直接引用Direct References):直接引用可以是直接指向目标的指针、相对偏移量或者一个能够间接定位到目标的句柄。与符号引用不同,直接引用与虚拟机的内存布局紧密相关。由于直接引用的实现并未在《Java虚拟机规范》中定义,因此同一个符号引用在不同虚拟机实现中转换得到的直接引用可能是不同的。

初始化

​ 初始化是类加载机制的最后一个阶段。在此阶段,JVM会执行开发者编写的静态代码块,并为静态变量(非常量)赋值。

​ 类的初始化方法对应于字节码中的<clinit>方法,静态代码块的执行以及静态变量赋值所生成的字节码指令都存储在该方法中。类的初始化是线程安全的,即<clinit>方法是加锁的,这确保了当多个线程加载同一个类时,初始化方法仅会执行一次。

case

​ 以下是一个代码示例,Foo类中定义了一个值为 1024 的常量unit,以及一个值为 100 的静态变量value。此外,Foo类的静态代码块将value的值更新为了 200,并打印了unit的值。

类初始化示例代码①

​ 在编译上述Foo类后,我们可以使用Jclasslib工具查看它的字节码信息,其中包含了类初始化方法<clinit>的字节码指令,如下图所示。

类初始化示例Jclasslib①

​ 接下来,我们可以基于《Java虚拟机规范》中对字节码指令的定义,解析上述初始化方法的字节码指令所代表的含义。

  1. bipush 100: 将byte数 100 推送到操作数栈的顶部。
  2. putstatic #2: 将操作数栈顶的值弹出,并赋给静态字段Foo.value。
  3. sipush 200: 将short数 200 推送到操作数栈的顶部。
  4. putstatic #2: 将操作数栈顶的值弹出,并赋给静态字段Foo.value。
  5. getstatic #3: 获取System类中的静态字段out,并将其推送到操作数栈的顶部。
  6. sipush 1024: 将short数 1024​ 推送到操作数栈的顶部。
  7. invokevirtual #5: 弹出操作数栈顶的对象地址,调用该对象的实例方法println。同时,依次弹出操作数栈中的元素,直至栈为空,按顺序将这些元素作为被调用方法的参数列表。
  8. return:结束当前方法的执行,并返回void。

​ 从Foo类的初始化方法的字节码指令可以看出,value在初始化后的值为 200,但是如果我们调换静态代码块与静态变量声明的顺序,最终的值仍然是 200 吗?

类初始化示例代码②

​ 在重新编译上述Foo类后,我们再次使用Jclasslib工具查看其初始化方法的字节码指令,如下图所示。

类初始化示例Jclasslib②

​ 显然,新的Foo类在初始化后,它的静态变量value的值为 100。由此,我们可以得出一个结论:当类初始化时,静态代码块和静态变量赋值的执行顺序取决于它们在代码中的声明位置。

​ 《Java虚拟机规范》规定了有且仅有 6 种情况必须对类进行初始化,这 6 种情况均涉及主动使用该类,具体如下。

  1. 当执行newgetstaticputstaticinvokestatic这四条字节码指令时,关联的类将被初始化。也就是说,当创建类的实例、访问类的静态变量(非常量)以及调用类的静态方法时,都会触发目标类的初始化。
  2. 使用java.lang.reflect包中的方法进行基于类的反射调用时,目标类将被初始化。例如调用Class.forName("...")newInstance()等方法。
  3. 在初始化一个类时,需先初始化其父类。
  4. Java虚拟机启动时,主类(即包含main方法的类)会被初始化。
  5. 当使用MethodHandleVarHandle句柄进行类调用时,关联的类会被初始化。这两个句柄操作提供了一种轻量级的反射机制,可以在运行时动态地操作类的方法和变量。
  6. 如果一个类实现了拥有默认方法的接口(这是Java 8引入的新特性),在初始化该类时,需先初始化其实现接口类。

​ 当一个类需要被初始化时,它必须从类加载阶段开始。这一过程是Java虚拟机确保类能够正常使用的重要前提,包含加载、连接和初始化三个阶段。

case

​ 以下是一个关于类初始化的练习,请你描述该程序运行后的输出内容。

java
public class Foo {
    static {
        System.out.println("initialize class Foo");
    }

    public static void main(String[] args) {
        print(Bar1.value, Bar3.value, new Bar3(), Bar3.value, Bar4.value()
                , Bar5.class, new Bar6[600]);
    }

    private static void print(Object... parameters) {
        for (Object parameter : parameters) {
            System.out.println(parameter);
        }
    }
}

class Bar1 {
    public static final int value = 100;

    static {
        System.out.println("initialize class Bar1");
    }
}

class Bar2 {
    public static int value = 200;

    static {
        System.out.println("initialize class Bar2");
    }
}

class Bar3 extends Bar2 {
    static {
        value = 300;
        System.out.println("initialize class Bar3");
    }
}

class Bar4 {
    public static int value() {
        return 400;
    }

    static {
        System.out.println("initialize class Bar4");
    }
}

class Bar5 {
    static {
        System.out.println("initialize class Bar5");
    }
}

class Bar6 {
    static {
        System.out.println("initialize class Bar6");
    }
}

​ 这段代码编译和运行后的结果如下图所示。

类初始化练习运行结果

​ 如果一个类没有静态代码块和静态变量赋值(非常量)的声明,那么其在编译后生成的字节码中就不会包含<clinit>方法,也就意味着该类在初始化时不会执行任何字节码指令。

case

​ 以下是一个代码示例,Foo类中定义了一个值为100的常量value1、一个未显式赋值的静态变量value2以及一个值为 "foo" 的常量foo。

类不存在初始化方法示例代码

​ 在编译上述Foo类后,我们可以使用Jclasslib工具查看它的字节码信息,其中包含该类的所有方法信息,如下图所示。

类不存在初始化方法示例Jclasslib

​ 从上图可以看出,Foo类中既没有静态代码块,也没有任何非常量的静态变量赋值操作,因此它编译后生成的字节码中不包含<clinit>方法,而<init>是该类的默认无参构造方法。

类加载器

类加载器介绍

​ 类加载器(ClassLoader)最初在JDK1.0中引入,它出现的初衷为支持Java Applet技术,但随着Java版本的升级迭代,它已经成为了Java语言不可或缺的一部分。

​ 类加载器是一种负责类加载的对象组件,它能够根据类的全限定名,通过各种渠道获取相应的字节码二进制流,并将其转换为JVM可识别的内部数据结构,存储在运行时数据区中。其中,开发者需要关注的是堆内存中的java.lang.Class对象。在Java中,无论是什么类型的类加载器,它们都继承自抽象类java.lang.ClassLoader

TIP

  1. 每个类对象(java.lang.Class对象)都持有一个指向加载它的ClassLoader对象的引用,该引用可以通过调用Class对象的getClassLoader()实例方法来获取。

  2. 数组类对象并非通过ClassLoader来加载生成,而是由JVM在分配数组内存空间后直接创建。

  3. 对于数组类对象,通过调用它的getClassLoader方法获取的ClassLoader对象与通过调用数组元素类型的Class对象的getClassLoader方法获取的ClassLoader对象是一致的。

  4. 在Java中,只有当全限定名和类加载器都完全相同的情况下,才能被视为同一个类,即对应同一个java.lang.Class对象。


​ JVM在启动时不会将所有类一次性加载到内存中,而是根据实际需要进行动态加载,这意味着大多数类只有在被使用时才会被加载。然而,对于一些系统运行所需的核心类,JVM会在启动阶段立即加载它们。

​ OpenJDK内置了多种类加载器,以满足不同的类加载需求,其中最重要的包括启动类加载器扩展类加载器应用程序加载器。启动类加载器位于HotSpot底层实现,使用C++语言编写;而扩展类加载器与应用程序类加载器则位于sun.misc内部包中,采用Java语言实现。

HotSpot重要的三个ClassLoader

case

​ 我们可以使用Arthas的classloader命令来列出当前Java进程(JDK1.8)中的类加载器。本示例采用的是最简单的Java进程,即主线程持续阻塞,没有任何处理逻辑。

Arthas的classloader命令

​ 可以看到,当前Java进程中确实存在启动类加载器、扩展类加载器和应用程序加载器,且它们各自的实例数量都为 1。此外,当前Java进程还存在一个ArthasClassLoader和多个DelegatingClassLoader类加载器实例。ArthasClassLoader是当前Java进程被Arthas进程attach后创建的类加载器。DelegatingClassLoader是OpenJDK内部为优化反射性能而提供的类加载器。简单来说,当一个方法被反射调用多次时,JVM会在运行时创建一个新的字节码类,以快速访问执行该方法。同时,JVM还会将新创建的类对应的类加载器设置为DelegatingClassLoader,以避免影响其他类在GC中的卸载。

​ 扩展类加载器和应用程序类加载器都是sun.misc.Launcher类中的静态内部类,它们的访问权限控制符均为缺省(包级私有)。这两个类加载器都是OpenJDK对java.lang.ClassLoader的具体内部实现,继承关系图如下。

ClassLoader继承关系图

sun.misc包中的API是OpenJDK的内部实现,它们并不属于Java标准,因此不建议直接使用其中的Launcher类来访问类加载器。相反,我们应该使用Java标准中的API来访问类加载器的相关信息,以下列出了几种常用的API。

  • java.lang.Class类中的实例方法getClassLoader()

  • java.lang.Thread类中的实例方法getContextClassLoader()

  • java.lang.ClassLoader中的静态方法getSystemClassLoader()

​ 需要注意的是,以上讨论仅适用于Java1.8及其之前版本的OpenJDK实现。随着Java9引入了模块化新特性,OpenJDK中的类加载器实现也发生了一些略微的变化,这部分内容将在下文中详细阐述。

启动类加载器

​ 启动类加载器(BootstrapClassLoader)由HotSpot虚拟机底层实现提供,是一款使用C++编写的类加载器,主要用于加载JDK的核心类。它默认加载JDK主目录的相对路径jre/lib/下的jar包,例如rt.jarresources.jarcharsets.jar等。其中,rt.jar中的rt表示Runtime,该jar包是Java的基础类库,包含java.xxx开头的标准库,比如java.util.*java.io.*java.nio.*java.lang.*java.sql.*java.math.*等。

TIP

​ 开发者无法在Java层面获取启动类加载器实例,它通常表示为null。

​ HotSpot虚拟机支持开发者使用参数-Xbootclasspath:<path>来显式指定启动类加载器的加载路径,但这种行为会导致原有路径被覆盖。为了解决这个问题,我们可以使用-Xbootclasspath/a:<path>的参数语法,其中/a代表新增路径。

TIP

​ 强烈不建议将需要启动类加载器扩展加载的jar包直接放入jre/lib/中,因为HotSpot虚拟机在使用BootstrapClassLoader来加载默认路径时,会进行多种规则校验。

​ 在大多数场景下,开发者无需关心启动类加载器的类加载过程。扩展和增强启动类加载器的类加载路径通常用于优化改善JDK基础设施。

扩展类加载器

​ 扩展类加载器(ExtClassLoader)是sun.misc.Launcher类中的一个静态内部类,主要用于加载JDK扩展功能类库中的类。它默认加载JDK主目录的相对路径jre/lib/ext下的jar包,例如nashorn.jarzipfs.jar等。其中,nashorn.jar是一个Java环境下的JS执行引擎类库,zipfs是一个支持zip文件系统的类库。

​ HotSpot虚拟机支持开发者使用系统变量-Djava.ext.dirs=<path>来显式指定扩展类加载器的加载路径,但这种行为会导致原有路径被覆盖。为了解决这个问题,我们可以在-Djava.ext.dirs变量的路径值中使用冒号符:来分隔多个路径。这样一来,我们既可以指定扩展类加载器的默认加载路径,也可以额外追加新的加载路径。

TIP

  1. 强烈不建议将需要扩展类加载器额外加载的jar包放入jre/lib/ext中。
  2. 在windows操作系统下,系统变量-Djava.ext.dirs的值使用分号符;来分隔路径。

应用程序类加载器

​ 应用程序类加载器(AppClassLoader)是sun.misc.Launcher类中的一个静态内部类,它负责加载类路径classpath下的jar包和类。当开发者使用Maven、Gradle等构建工具时,这些工具会将项目所依赖的jar包添加到classpath中,因此应用程序类加载器也会加载这些依赖库中的类。

​ HotSpot虚拟机支持开发者使用系统变量-Djava.class.path=<path>来显式指定应用程序类加载器的加载路径,但这种行为会导致原有路径被覆盖。为了解决这个问题,我们可以在-Djava.ext.dirs变量的路径值中使用冒号符:来分隔多个路径。这样一来,我们既可以指定当前项目的工作路径,也可以额外追加新的加载路径。

TIP

  1. 在windows操作系统下,系统变量-Djava.class.path的值使用分号;来分隔路径。
  2. Oracle官方更推荐使用java命令的-classpath <path>来指定类路径,而不是使用系统变量-Djava.class.path=<path>
  3. 在IntelliJ IDEA中运行Java程序时,它会自动传入-classpath参数,其值包括jre/libjre/lib/ext下的所有jar包,以及当前项目的工作路径。

自定义类加载器

​ 自定义类加载器是由开发者自行实现的类加载器,目的是满足特殊需求和扩展功能。通常,这种类加载器会继承java.lang.ClassLoader,该抽象类已经提供了类加载相关的核心方法,开发者仅需实现相应的模版方法即可。

​ ClassLoader类中的核心方法及其详细说明如下图所示。其中,loadClass方法是整个类加载流程的入口,它提供了双亲委派机制,该机制的相关内容将在下文中详细说明;findClass是一个需要开发者实现的模版方法,其默认实现为直接抛出异常,它的主要目标是返回指定类的Class对象引用,该过程通常需要依赖调用内部的defineClass方法来完成;resolveClass方法用于执行类的连接阶段,它内部会直接调用本地方法,并且不会触发类的初始化;defineClass方法用于根据字节码的字节流在运行时数据区创建JVM内部可识别的数据结构,并返回该类在堆上的Class对象引用。

ClassLoader类中有关类加载的核心方法.png

​ 实现自定义类加载器的最佳实践通常是只重写ClassLoader中的findClass方法,保留双亲委派机制,同时避免触发类的连接。在findClass方法中,开发者首先需要根据实际需求,从特定渠道获取指定类的字节码数据流,其次通过调用defineClass方法来生成JVM内部的数据结构,最后返回堆上的Class对象引用。

双亲委派模型

类加载器的parent

​ 每个类加载器都拥有一个父加载器parent。前文提到的三个类加载器都是在JVM启动时默认创建的,并且它们都是单例。其中,启动类加载器是层级结构中的最高层,扩展类加载器的parent是启动类加载器,而应用程序类加载器的parent则是扩展类加载器。

类加载器的parent

​ 自定义类加载器的parent默认为应用程序类加载器,但开发者可以根据实际需求,通过调用父类java.lang.ClassLoader的构造方法来显式指定当前类加载器的parent。

显式指定类加载器的parent

case

​ 以下是一个代码示例,该示例通过while循环来打印系统中各个类加载器之间的父子关系。

类加载器parent示例代码

​ 在上述代码中,Thread.currentThread().getContextClassLoader()方法默认返回应用程序类加载器,除非显式指定当前线程的类加载器。因此,程序运行后最终会打印出应用程序类加载器、扩展类加载器的信息。需要注意的是,启动类加载器使用C++编写,位于JVM底层实现,开发者无法在Java层面获取其实例,通常表示为null。

类加载器parent示例结果

工作模型

​ 类加载器的双亲委派机制由ClassLoader抽象类中的loadClass方法提供,以下是该方法的源码(OpenJDK1.8)。

loadClass方法源码

​ 可以看到,loadClass方法的默认实现相对简单,其具体逻辑可以分为以下几个步骤。

  1. 尝试获取锁资源,锁由getClassLoadingLock方法提供。尽管该方法可以被重写,但其默认实现相对完善,通常无需进行重新实现。

  2. 调用findLoadedClass方法以检查指定类是否已经被当前类加载器加载过。

  3. 如果当前类加载器尚未加载过指定类,则会尝试调用其parent的loadClass方法来加载该类,若parent的值为null,那么意味着parent是启动类加载器,此时将直接调用findBootstrapClassOrNull方法来尝试加载该类。findBootstrapClassOrNull方法内部会调用本地方法以与HotSpot虚拟机底层的启动类加载器交互。需要注意的是,如果在parent的类加载过程中抛出了ClassNotFoundException异常,那么当前类加载器会忽略这个异常,并选择由自身来尝试加载(可参考以下步骤)。

  4. 经过以上步骤后,如果指定类仍然没有被加载,则会调用当前类加载器的findClass方法来尝试加载该类。同时,此步骤还会记录类加载的相关数据。

  5. 判断指定类是否需要进行连接,如果条件成立,则调用resolveClass方法以连接该类。

  6. 返回加载后指定类在堆上的Class对象引用c。需要注意的是,此时c可能为null,因此子类的最佳做法是在findClass中,如果类未找到,则直接抛出ClassNotFoundException异常。

​ 通过对上述步骤的分析,我们可以得出一个结论:loadClass方法采用了递归回溯的方式来进行类加载。在类加载过程中,如果将findBootstrapClassOrNull视作一个本地的loadClass方法,那么loadClass方法会递归至启动类加载器,并回溯至入口类加载器。递归过程沿着类加载器的parent链向上查询指定类是否已加载,而回溯过程则沿着同一链向下尝试加载该类。这种向上查询、向下加载的机制通常被称为双亲委派模型Parents Delegation Model)。

双亲委派模型

​ 双亲委派模型的主要优势包括以下两点。

  1. 确保Java核心类库不遭到篡改,从而提升系统的安全性。
  2. 防止类被重复加载,保持加载效率和一致性。

打破双亲委派

​ 在某些特殊需求和场景下,开发者可能需要打破类加载中的双亲委派机制,以下列出了三种常见的方式。需要注意的是,实际上只有一种方式能够真正打破双亲委派机制,那就是自定义类加载器。

打破双亲委派的方式

  1. 自定义类加载器:通过重写loadClass方法来覆盖双亲委派的逻辑。例如,Tomcat使用自定义类加载器来实现应用间的隔离,确保每个应用都有其独立的类加载环境。
  2. 线程上下文类加载器:直接利用线程上下文类加载器来加载特定的类,例如JDBC驱动的注册,这种方式实际上并没有打破双亲委派机制。以JDBC驱动的注册为例,虽然DriverManager类是由启动类加载器加载的,但在DriverManager初始化时,具体的实现类是通过线程上下文类加载器进行加载的,此时仍然遵循双亲委派规则。
  3. OSGI框架:利用OSGI模块化框架,可以实现同级类加载器之间的相互委托来进行类加载。然而,这种方式本质上仍然是OSGI通过自定义类加载器来实现的一套完整的模块化类加载机制。

​ 尽管开发者可以通过自定义类加载器来打破双亲委派机制,但仍然无法篡改Java中的核心类。这是因为defineClass方法内部会调用preDefineClass方法进行校验。此外,defineClass方法本身是不可重写的。

defineClass方法中的校验

JDK9的新变化

​ JDK9引入了模块化的新特性,类的管理不再使用jar包,而是通过jmod模块进行管理。这些jmod模块文件存储在JDK主目录下的相对路径jmods中,如下图所示。其中,java.base.jmod的作用与JDK9之前的rt.jar相同,用于存储JVM运行时所需的核心类,例如java.*包中的类。

jmods

​ JDK9引入模块化概念后,系统内置的类加载器设计发生了显著变化:启动类加载器不再由C++实现,而是转而使用Java实现,它与平台类加载器、应用程序类加载器三者都是jdk.internal.loader.ClassLoaders类中的静态内部类。平台类加载器是对旧版本的兼容,即替换JDK9之前的扩展类加载器,其自身无特殊的逻辑。

JDK9中的ClassLoaders

​ 启动类加载器(BootClassLoader)、平台类加载器(PlatformClassLoader)与应用程序类加载器(AppClassLoader)都继承自BuiltinClassLoader,该父类为其子类提供了从特定模块加载类资源的功能。与JDK9之前版本相比,类加载器继承关系的唯一变化为UrlClassLoaderBuiltinClassLoader取代,具体的继承关系如下图所示。

JDK9中ClassLoader继承图

TIP

  1. 为了保持与JDK9之前版本的兼容性,启动类加载器实例在Java层面仍然无法获取,依旧表示为null。
  2. 与JDK9之前版本一致,应用程序类加载器的parent是平台类加载器,平台类加载器的parent是启动类加载器(null),启动类加载器位于parent链的最顶层。

上次更新于: