AI生成-Java 类加载器机制总结

Java 类加载器机制总结

一看就懂,详解Java中的类加载器机制,附热加载示例代码演示_哔哩哔哩_bilibili

理解 ClassLoader 是掌握 Java 热更新、模块隔离、SPI 机制的基础。本文梳理核心概念、类加载器体系、双亲委派模型、线程上下文类加载器,以及它们在热更新中的应用。


一、什么是 ClassLoader?

ClassLoader 的作用是把 .class 文件的字节码加载到 JVM 中,生成 Class<?> 对象。

Java 中类的唯一性由「全限定名 + ClassLoader 实例」共同决定。同一个类文件,用不同的 ClassLoader 加载,JVM 会认为是两个不同的类。这正是热更新的核心原理。


二、JDK 内置的三大类加载器

类加载器 实现 加载路径 说明
启动类加载器 (Bootstrap ClassLoader) C++ 实现,Java 中没有对应类 $JAVA_HOME/jre/lib/rt.jarresources.jar 加载 JVM 核心类库(java.lang.*java.util.*
扩展类加载器 (Extension ClassLoader) sun.misc.Launcher$ExtClassLoader $JAVA_HOME/jre/lib/ext/ 加载 JDK 扩展包(JDK 9+ 被 PlatformClassLoader 取代)
应用程序类加载器 (Application/System ClassLoader) sun.misc.Launcher$AppClassLoader classpath 下所有类 加载业务代码和第三方 jar

三者关系(双亲委派模型):

         Bootstrap ClassLoader          ← 最顶层(C++ 实现)
                  ↑
         Extension ClassLoader          ← parent 字段 = null(但委托给 Bootstrap)
                  ↑
         Application ClassLoader        ← parent = ExtClassLoader(JVM 启动时显式设置)
                  ↑
         自定义 ClassLoader              ← parent = AppClassLoader(用户指定或默认)

三、双亲委派模型

3.1 加载流程

类加载请求遵循自下而上委托,自上而下查找的原则:

某个 ClassLoader.loadClass("com.example.Foo")
    │
    ├─ 1. findLoadedClass("com.example.Foo")
    │      → 已加载?直接返回
    │
    ├─ 2. parent.loadClass("com.example.Foo")      ← 向上委托
    │      → 父加载器继续向上委托
    │      → 直到 Bootstrap
    │
    ├─ 3. Bootstrap 从核心库查找
    │      → 找到?加载返回
    │      → 没找到?逐层向下
    │
    ├─ 4. 每一层尝试 findClass("com.example.Foo")
    │      → 找到?加载返回
    │
    └─ 5. 都找不到?抛出 ClassNotFoundException

3.2 核心代码(ClassLoader.loadClass)

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    synchronized (getClassLoadingLock(name)) {
        // 1. 检查是否已加载
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            try {
                // 2. 委托父加载器
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    // parent 为 null → 走 Bootstrap
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // 父加载器找不到,继续
            }
            if (c == null) {
                // 3. 自己加载
                c = findClass(name);
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

3.3 为什么需要双亲委派?

目的 说明
避免重复加载 父加载器加载过的类,子加载器不再加载,保证核心类全局唯一
安全隔离 防止自定义 ClassLoader 加载伪造的 java.lang.String,篡改核心 API
类型安全 保证同一个 Class 对象在 JVM 中只存在一份,instanceof、类型转换不出错

四、ClassLoader 的 parent 是怎么来的?

4.1 构造函数决定

// ClassLoader 源码(简化)
protected ClassLoader(ClassLoader parent) {
    this.parent = parent;           // 你显式指定
}

protected ClassLoader() {
    // 没传 parent → 自动取系统类加载器
    this.parent = getSystemClassLoader();  // = AppClassLoader
}

4.2 各加载器的 parent 来源

加载器 parent 怎么来的
Bootstrap 无 parent,C++ 实现
ExtClassLoader JVM 启动时硬编码,parent 字段 = null(实际委托 Bootstrap)
AppClassLoader JVM 启动时 new AppClassLoader(extCL),显式传入
你的 HotSwapClassLoader super(ctxClassLoader),显式传入
new ClassLoader()(无参) JVM 自动取 getSystemClassLoader() = AppClassLoader

4.3 parent = null 的特殊含义

ExtClassLoader 的 parent 字段是 null,但这不是"没有父加载器",而是"委托给 Bootstrap":

if (parent != null) {
    c = parent.loadClass(name, false);   // 正常委托
} else {
    c = findBootstrapClassOrNull(name);   // null → 走 Bootstrap(JNI 调用)
}

五、线程上下文类加载器(ContextClassLoader)

5.1 是什么?

Thread.currentThread().getContextClassLoader() 返回的是线程级别的一个 ClassLoader 引用,默认是 AppClassLoader。它本质上就是一个可随时读写的线程属性。

5.2 为什么需要它?

双亲委派模型有个死穴:父加载器加载的类,看不到子加载器加载的类。但 Java SPI 机制(如 JDBC)正好反过来:

JDK 的 JDBC 接口 (java.sql.Driver)  ← Bootstrap 加载
        ↑ 需要调用 ↓
MySQL 驱动实现 (com.mysql.Driver)   ← AppClassLoader 加载

Bootstrap 加载的 DriverManager 无法通过双亲委派找到 classpath 下的 MySQL 驱动。解决方案就是通过 Thread.currentThread().getContextClassLoader() —— 线程属性不受双亲委派限制,可以"向下"访问 AppClassLoader 里的类。

5.3 默认值与继承规则

场景 ContextClassLoader
main 线程 AppClassLoader(JVM 启动时设置)
子线程(new Thread()) 继承父线程的 ContextClassLoader
线程池线程 取决于池创建时所在线程的 CCL
可随时修改 Thread.currentThread().setContextClassLoader(xxx)

5.4 容器/框架经常修改它

场景 ContextClassLoader 变化
Tomcat Web 应用 每个 WebApp 有独立的 WebappClassLoader,Tomcat 会把请求线程的 CCL 切到对应 WebApp 的加载器
Spring Boot DevTools RestartClassLoader 替换默认的 AppClassLoader
OSGi 容器 每个 Bundle 有独立 ClassLoader,容器在进入 Bundle 代码前切换 CCL
热更新方案 加载热更类时临时 setContextClassLoader(hotSwapClassLoader),让脚本正确解析依赖

5.5 验证代码

public class TestCCL {
    public static void main(String[] args) {
        // main 线程 → AppClassLoader
        System.out.println("main: " + Thread.currentThread().getContextClassLoader());
        
        // 修改 main 线程的 CCL
        Thread.currentThread().setContextClassLoader(null);
        System.out.println("main改后: " + Thread.currentThread().getContextClassLoader());
        
        // 子线程 → 继承父线程的 CCL(此时是 null)
        new Thread(() -> {
            System.out.println("子线程: " + Thread.currentThread().getContextClassLoader());
        }).start();
    }
}

六、热更新中的 ClassLoader 实践

6.1 核心思路:打破双亲委派

热更新的必要条件:每次热更必须用全新的 ClassLoader 实例。
ClassLoader 的核心职责就两个:创建 Class 对象 + 缓存 Class 对象

自定义 ClassLoader 重写 loadClass(),让特定包下的实现类由自己加载,其他类继续走双亲委派:

public class HotSwapClassLoader extends ClassLoader {
    
    private final File classDir;
    
    public HotSwapClassLoader(File classDir) {
        // 父加载器 = 线程上下文类加载器(默认 AppClassLoader)
        super(Thread.currentThread().getContextClassLoader());
        this.classDir = classDir;
    }
    
    @Override
    protected Class<?> loadClass(String name, boolean resolve) 
            throws ClassNotFoundException {
        // 1. 检查是否已加载
        Class<?> c = findLoadedClass(name);
        if (c != null) return c;
        
        // 2. 只有特定包下的具体实现类才从本地加载(打破委派)
        if (name.startsWith("com.game.script.")) {
            String simpleName = name.substring(name.lastIndexOf('.') + 1);
            // 接口和注解交给父加载器
            if (!isInterface(simpleName) && !isAnnotation(simpleName)) {
                byte[] bytes = readFile(new File(classDir, simpleName + ".class"));
                c = defineClass(name, bytes, 0, bytes.length);
                if (resolve) resolveClass(c);
                return c;
            }
        }
        
        // 3. 其他类委托父加载器(保持双亲委派)
        return super.loadClass(name, resolve);
    }
}

6.2 关键设计

要点 说明
接口由父加载器加载 保证接口全局唯一,instanceof 判断不出错
实现类由子加载器加载 每次创建新 ClassLoader → 新的类 → 热更
父加载器 = AppClassLoader 框架类、JDK 类走双亲委派,无需操心
旧 ClassLoader 失去引用 被 GC 回收,旧类随之卸载

6.3 热更时 ContextClassLoader 的作用

热更脚本内部如果通过 SPI、反射等方式加载依赖类,需要把当前线程的 CCL 临时切到 HotSwapClassLoader:

public void reloadScript() {
    ClassLoader oldCCL = Thread.currentThread().getContextClassLoader();
    HotSwapClassLoader hotCL = new HotSwapClassLoader(hotSwapDir);
    
    try {
        // 临时切换,让脚本内部的类加载走热更加载器
        Thread.currentThread().setContextClassLoader(hotCL);
        
        Class<?> clazz = hotCL.loadClass("com.game.script.SkillScript");
        Object script = clazz.newInstance();
        // ... 替换缓存中的旧实例
        
    } finally {
        // 恢复
        Thread.currentThread().setContextClassLoader(oldCCL);
    }
}

七、ClassLoader 核心特性总结

特性 说明
可见性(自上而下) 父加载器加载的类对子加载器可见,反之不行
唯一性 类唯一性 = 全限定名 + ClassLoader 实例
命名空间隔离 每个 ClassLoader 有独立的类命名空间
热更新基础 不同 ClassLoader → 不同类 → 旧加载器回收 → 类被卸载
parent 来源 要么显式传入,要么 JVM 自动取 AppClassLoader
ContextClassLoader 线程级属性,默认继承,可随时修改,用于打破双亲委派限制

八、常见问题

Q1: 所有线程的 ContextClassLoader 都是 AppClassLoader 吗?

不是。子线程继承父线程的 CCL,Tomcat/OSGi 等容器会主动切换它,你也可以随时 setContextClassLoader()

Q2: ClassLoader 的 parent 是 JVM 默认设置的吗?

是的。三大内置加载器的 parent 由 JVM 启动时硬编码设定;自定义 ClassLoader 的 parent 要么你显式传,要么 JVM 自动取 getSystemClassLoader()(即 AppClassLoader)。

Q3: parent = null 是什么意思?

不是没有父加载器,而是委托给 Bootstrap ClassLoaderClassLoader.loadClass()parent == null 时会走 findBootstrapClassOrNull()

Q4: 热更脚本内部的依赖类加载不对怎么办?

临时将当前线程的 ContextClassLoader 设为 HotSwapClassLoader,确保脚本内部通过 CCL 加载的依赖类走热更加载器。


本文基于 ClassLoader 机制原理及实际热更新项目经验整理。

posted @ 2026-06-27 14:27  deyang  阅读(3)  评论(0)    收藏  举报