源本科技 | 码上会

Java 中的类加载器

2025/12/22
13
0

引言

Java 类加载器(ClassLoader)是 Java 运行时环境(JRE)的核心组成部分,负责将 Java 类动态加载到 Java 虚拟机(JVM)中。由于类加载器的存在,Java 运行时系统无需直接了解底层的文件或文件系统细节

Java 类并非在程序启动时一次性全部加载到内存中,而是按需加载——即当应用程序首次使用某个类时,JRE 才会调用相应的 ClassLoader 将该类加载进内存

这种机制使 ClassLoader 在 Java 的动态类加载能力中扮演了关键角色,不仅提升了内存使用效率,还增强了应用程序的灵活性和可扩展性

类加载器类型

Java 的类加载器(ClassLoaders)分为多种类型,每种负责从特定位置加载类:

启动类加载器

启动类加载器(Bootstrap ClassLoader,又称 Primordial ClassLoader)

  • 启动类加载器由本地机器代码(通常是 C/C++ 实现)构成,是 JVM 启动时最早运行的组件

  • 在 Java 8 及更早版本中,它从 rt.jar 等核心 JAR 文件中加载 Java 基础类库(如 java.lang.*java.util.* 等)

  • Java 9 开始,由于引入了模块化系统(Jigsaw),核心类库被打包进 Java 运行时镜像(Java Runtime Image, JRT),启动类加载器改从此处加载模块化的平台类

  • 它没有父类加载器,在类加载器层次结构中处于最顶层

平台类加载器

平台类加载器(Platform Class Loader)

  • 在 Java 9 之前,这一角色由 扩展类加载器(Extension ClassLoader) 承担,负责加载 $JAVA_HOME/jre/lib/ext 目录下的扩展 JAR

  • 自 Java 9 起,扩展机制被弃用,该加载器更名为 平台类加载器(Platform Class Loader),用于加载 JDK 模块系统中的平台模块(如 java.sqljava.xml 等)

  • 它从 Java 运行时镜像或通过 --module-path 或系统属性(如 java.platform)指定的模块路径中加载类

系统类加载器

系统类加载器(System ClassLoader,又称应用类加载器 Application ClassLoader)

  • 负责加载应用程序自身的类,即开发者编写的代码以及第三方依赖

  • 它是平台类加载器的子加载器

  • 加载路径由以下方式指定:

    • 环境变量 CLASSPATH

    • 命令行参数 -classpath-cp

    • 默认当前目录(若未显式指定)

这种分层结构遵循 双亲委派模型(Parent Delegation Model):当一个类加载器收到加载请求时,会先委托其父加载器尝试加载,只有在父加载器无法完成时,才由自己加载。这一机制保障了 Java 核心类库的安全性和一致性

类加载器原理

Java 类加载器(ClassLoaders)的运作遵循以下核心原则:

委托模型

类加载器采用双亲委派机制(Parent Delegation Model):

  • 当 JVM 需要加载某个类时,首先检查该类是否已加载。若未加载,则从应用类加载器(Application ClassLoader)开始,逐级向上委托——先交由其父加载器(平台类加载器),再由平台类加载器委托给启动类加载器(Bootstrap ClassLoader)

  • 每个类加载器首先尝试让其父加载器加载类;只有当父加载器无法找到该类时,当前加载器才会尝试自己加载

  • 这一机制确保了核心 Java 类(如 java.lang.Object)始终由最上层的 Bootstrap ClassLoader 加载,防止被用户自定义类篡改,从而保障系统安全与一致性

可见性原则

可见性原则(Visibility Principle)

  • 子类加载器可以访问由其父类加载器加载的类,但父类加载器无法访问子类加载器加载的类

  • 例如,应用类加载器加载的类可以使用 java.util.ArrayList(由 Bootstrap 或 Platform 加载),但反之则不行

  • 此设计增强了模块隔离性,避免不同来源的同名类相互干扰

唯一性原则

唯一性原则(Uniqueness Property)

  • 每个类在 JVM 中由“类加载器实例 + 全限定类名”共同唯一标识

  • 由于委托机制的存在,一个类只会被最先能够加载它的类加载器加载一次,后续请求直接返回已加载的类,避免重复加载和潜在冲突

类加载器的方法

java.lang.ClassLoader 中提供了多个用于加载类的方法:

  • loadClass(String name, boolean resolve):由 JVM 调用以加载所引用的类,并在必要时进行解析(resolve)

  • defineClass():将字节数组定义为一个类的实例;如果该字节数组不符合有效的类文件格式,则抛出 ClassFormatError

  • findClass(String name):查找指定名称的类,但不触发其加载(通常由子类重写以实现自定义加载逻辑)

  • findLoadedClass(String name):检查指定名称的类是否已被当前类加载器加载

  • Class.forName(String name, boolean initialize, ClassLoader loader):使用指定的 ClassLoader 加载并(可选)初始化类。若传入的 ClassLoadernull,则默认使用 Bootstrap 类加载器

示例:

// 在类加载前执行的代码
Class<?> clazz = ClassLoader.getSystemClassLoader().loadClass("com.example.MyClass");

功能原理

委托模型

Java 虚拟机(JVM)和 ClassLoader 使用一种称为委托层次算法(Delegation Hierarchy Algorithm)的机制来加载类。其工作流程如下:

  • ClassLoader 始终遵循委托层次原则。

  • 当 JVM 遇到一个类时,首先检查该类是否已加载到方法区(Method Area)。

    • 如果已加载,则直接继续执行。

    • 如果未加载,则 JVM 通知 ClassLoader 子系统加载该类,并将控制权交给 Application ClassLoader(也称 System ClassLoader)。

  • Application ClassLoader 并不立即尝试加载类,而是将请求向上委托给其父加载器:

    • 首先委托给 Extension ClassLoader

    • Extension ClassLoader 再委托给 Bootstrap ClassLoader

  • Bootstrap ClassLoader 在 Bootstrap 类路径(通常是 JDK/jre/lib 或模块路径)中查找类:

    • 若找到,则加载该类;

    • 若未找到,则将控制权交还给 Extension ClassLoader。

  • Extension ClassLoader 在 扩展类路径(如 JDK/jre/lib/ext 或通过 java.ext.dirs 指定的目录)中查找:

    • 若找到,则加载;

    • 否则,再将控制权交还给 Application ClassLoader。

  • Application ClassLoader 最后在 应用程序类路径(即 -classpath-cp 指定的路径)中查找:

    • 若找到,则加载;

    • 若仍未找到,则抛出 ClassNotFoundException

注意:从 Java 9 开始,jre/lib/ext 目录和 java.ext.dirs 系统属性已被弃用,Extension ClassLoader 的作用大幅弱化,但委托模型的基本逻辑仍保持不变

可见性原则

该原则规定:

  • 父加载器加载的类对子加载器可见

  • 子加载器加载的类对父加载器不可见

例如,若 Hello.class 由 Extension ClassLoader 加载,则 Application ClassLoader 可以访问它,但 Bootstrap ClassLoader 无法访问。如果 Bootstrap ClassLoader 尝试再次加载该类,将因找不到而抛出 java.lang.ClassNotFoundException

唯一性原则

该原则确保:

  • 每个类在 JVM 中只被加载一次;

  • 子加载器不会重复加载已被父加载器成功加载的类。

具体而言,只有当父加载器无法找到某个类时,当前 ClassLoader 实例才会尝试自己加载。这避免了类的重复定义和潜在的类型冲突(如 ClassCastExceptionLinkageError

关键方法

在 JVM 请求加载某个类后,需遵循一系列步骤完成类的加载。尽管整个过程基于委托模型(Delegation Model),但以下几个核心方法在类加载机制中起着关键作用:

  • loadClass(String name, boolean resolve)
    该方法用于加载 JVM 所引用的类,接收类的全限定名作为参数。若 resolvetrue,则在加载后还会对类进行解析(例如链接阶段中的符号引用解析)。这是 ClassLoader 的入口方法,通常不应被子类重写

  • defineClass()
    这是一个 final 方法,不可被重写。它将一个字节数组(代表有效的 .class 文件内容)转换为 Class 对象。如果字节码不符合 JVM 规范,则抛出 ClassFormatError

  • findClass(String name)
    此方法用于查找并定义指定名称的类,但不触发委托或解析。通常由自定义 ClassLoader 重写此方法,以实现特定的类查找逻辑(如从网络、加密文件等加载字节码)

  • findLoadedClass(String name)
    检查当前 ClassLoader 是否已加载过指定名称的类。若已加载,则直接返回对应的 Class 对象,避免重复加载

  • Class.forName(String name, boolean initialize, ClassLoader loader)
    该静态方法不仅加载类,还可选择是否执行初始化(即执行 <clinit> 静态初始化块)。通过 loader 参数可显式指定使用哪个 ClassLoader。若传入 null,则默认使用 Bootstrap ClassLoader(注意:此时返回的 Class 对象的 getClassLoader() 将为 null

典型实现逻辑

以下代码展示了 loadClass 方法的标准流程(在 Java 8 及更早版本中常见):

protected synchronized Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException {
    
    // 1. 检查类是否已加载
    Class<?> c = findLoadedClass(name);
    
    if (c == null) {
        try {
            // 2. 委托给父加载器
            if (parent != null) {
                c = parent.loadClass(name, false);
            } else {
                // 若无父加载器,则委托给 Bootstrap ClassLoader
                c = findBootstrapClass0(name);
            }
        } catch (ClassNotFoundException e) {
            // 父加载器未找到类,继续由当前加载器处理
        }
        
        // 3. 若父加载器未加载成功,则调用 findClass 自行加载
        if (c == null) {
            c = findClass(name);
        }
    }
    
    // 4. 若需要解析,则进行解析
    if (resolve) {
        resolveClass(c);
    }
    
    return c;
}

说明

  • 如果类已加载,直接返回

  • 否则,优先委托给父 ClassLoader(遵循委托模型)

  • 仅当父加载器无法加载时,才调用 findClass() 由当前加载器尝试加载

  • 最后根据 resolve 参数决定是否调用 resolveClass() 进行解析