一、引言
在 Java 开发中,我们每天都在写类、调用类、实例化对象,但你有没有想过:一个 .class 文件是如何被 JVM 加载进内存,并最终变成一个可以使用的 Java 对象的?
这个过程的核心,就是 类加载机制(Class Loading Mechanism),而其中最核心、最经典的设计,就是我们今天要讲的 —— 双亲委派机制(Parent Delegation Model)。
理解双亲委派,不仅能帮助你深入 JVM 底层,更能帮你解决实际开发中遇到的类冲突、ClassNotFoundException、类隔离、热部署等问题。
二、什么是“双亲委派”?
“双亲委派”这个名字听起来有点奇怪 —— Java 类加载器难道有两个“父亲”?其实不是。“双亲”是英文 “Parent” 的直译,更准确的说法应该是 “父级委派机制”。
它的定义非常简单:
当一个类加载器收到类加载请求时,它首先不会自己去加载这个类,而是把这个请求“委托”给它的父类加载器去完成。每一层都是如此,直到最顶层的启动类加载器。只有当父加载器无法加载该类时(即在其搜索路径中找不到),子加载器才会尝试自己加载。
这是一种“自底向上委托,自顶向下加载”的机制。
三、Java 类加载器的层级结构
在 JVM 中,默认存在三层类加载器,构成一棵“树”:
1. 启动类加载器(Bootstrap ClassLoader)
- 由 C++ 实现,是 JVM 的一部分。
- 负责加载 Java 核心类库,如 rt.jar、java.lang.*、java.util.* 等。
- 路径:$JAVA_HOME/jre/lib
- 它是所有类加载器的“根”,没有父加载器。
2. 扩展类加载器(Extension ClassLoader)
- 由 Java 实现(sun.misc.Launcher$ExtClassLoader)。
- 负责加载扩展目录中的类:$JAVA_HOME/jre/lib/ext 或 java.ext.dirs 指定路径。
- 父加载器:Bootstrap ClassLoader。
3. 应用程序类加载器(Application ClassLoader / System ClassLoader)
- 由 sun.misc.Launcher$AppClassLoader 实现。
- 负责加载我们自己写的类,即 ClassPath 下的类。
- 父加载器:Extension ClassLoader。
- 也是我们自定义类加载器默认的“父亲”。
开发者也可以继承 java.lang.ClassLoader 实现自定义类加载器,通常以 Application ClassLoader 为父。
四、双亲委派的工作流程(图解+伪代码)
伪代码简化版:
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 {
// 3. 若无父加载器,则委托给 Bootstrap
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 父加载器找不到,继续往下走
}
// 4. 父加载器都失败,自己加载
if (c == null) {
c = findClass(name); // 自定义加载逻辑
}
}
if (resolve) {
resolveClass(c); // 链接类
}
return c;
}
}流程图解:
用户自定义 ClassLoader
↓ 委托
Application ClassLoader
↓ 委托
Extension ClassLoader
↓ 委托
Bootstrap ClassLoader → 加载?→ 成功 → 返回
↓
失败 → 逐级返回 → 子加载器自己加载五、为什么要有双亲委派?三大核心优势
1. 避免类的重复加载
同一个类被不同 ClassLoader 加载,会被 JVM 视为完全不同的两个类(类的唯一性 = 全限定名 + 加载器)。双亲委派确保一个类只会被“最上层能加载它的加载器”加载一次,避免重复。
2. 保证核心类库的安全性
试想:如果你在项目里写了一个 java.lang.String 类,放在 ClassPath 下,会不会替换掉 JDK 的 String?
不会! 因为双亲委派机制会让 Bootstrap ClassLoader 优先加载官方的 String,你的“山寨版”根本没机会被加载 —— 有效防止核心 API 被污染或劫持。
3. 保证 Java 程序的稳定性和一致性
所有类加载都有统一入口和顺序,避免混乱。比如 Object 类无论在哪里使用,都是由 Bootstrap 加载的同一个类,确保 instanceof、反射、序列化等行为一致。
六、什么时候需要“打破”双亲委派?
虽然双亲委派是默认机制,但在某些高级场景下,必须“打破”它:
1. SPI(Service Provider Interface)机制 —— 最经典的例子:JDBC
- java.sql.Driver 是 JDK 核心接口,由 Bootstrap 加载。
- 但 MySQL、PostgreSQL 的驱动实现类在第三方 jar 中,由 AppClassLoader 加载。
- Bootstrap 无法加载 AppClassLoader 的类 → 矛盾!
解决方案:使用 线程上下文类加载器(Thread.currentThread().getContextClassLoader()),让高层类加载器能“向下”加载低层类。
// 示例:JDBC 加载驱动时
Class.forName("com.mysql.cj.jdbc.Driver", true,
Thread.currentThread().getContextClassLoader());2. OSGi、插件化架构、热部署系统
- 如 Eclipse 插件、阿里 SOFAArk、Spring Boot DevTools。
- 需要实现模块隔离、动态加载/卸载,每个模块有独立 ClassLoader。
- 必须自定义类加载逻辑,不完全遵守双亲委派。
3. 自定义 ClassLoader 重写loadClass()
如果你继承 ClassLoader 并重写了 loadClass() 方法,改变了“先委托父类”的顺序,比如先自己加载,再委托父类 —— 就打破了双亲委派。
注意:除非明确知道自己在做什么,否则不建议随意打破双亲委派,容易引发类冲突或内存泄漏。
七、实战小例子:验证双亲委派
你可以写一个简单的测试类:
public class TestClassLoader {
public static void main(String[] args) {
System.out.println("String 类加载器:" + String.class.getClassLoader()); // null → Bootstrap
System.out.println("自定义类加载器:" + TestClassLoader.class.getClassLoader()); // AppClassLoader
}
}输出:
String 类加载器:null
自定义类加载器:sun.misc.Launcher$AppClassLoader@xxxxxx为什么 String.class.getClassLoader() 是 null?因为 Bootstrap 是 C++ 实现的,Java 里拿不到引用,所以返回 null —— 但它确实是“最高领导”。
八、总结:一张图 + 三句话记住双亲委派
一张图:
[自定义ClassLoader] → 委托 → [AppClassLoader] → 委托 → [ExtClassLoader] → 委托 → [Bootstrap]
↓
加载核心类
↓ 失败?
[自定义ClassLoader] ← 返回 ← [AppClassLoader] ← 返回 ← [ExtClassLoader] ← 返回 ← 失败
↓
自己尝试加载三句话总结:
- 向上委托,向下加载 —— 先让“爸爸”试试,不行自己上。
- 安全第一,避免重复 —— 核心类不被污染,类不重复加载。
- 默认机制,可被打破 —— SPI、插件化、热部署等场景需灵活处理。