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.jar、resources.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 ClassLoader。ClassLoader.loadClass() 中 parent == null 时会走 findBootstrapClassOrNull()。
Q4: 热更脚本内部的依赖类加载不对怎么办?
临时将当前线程的 ContextClassLoader 设为 HotSwapClassLoader,确保脚本内部通过 CCL 加载的依赖类走热更加载器。
本文基于 ClassLoader 机制原理及实际热更新项目经验整理。
浙公网安备 33010602011771号