Java JVM 双亲委派(Parent Delegation)机制
本文旨在把 JVM 双亲委派(Parent Delegation)机制 做一个系统、源码级别且实用的详解:为什么要有它、在 Java 层大致实现是什么样子、热点实现细节(包括并行加载的演进)、常见变体(child-first/容器例外)与常见陷阱(类身份、ClassCastException、类泄漏、TCCL 问题),并配上 mermaid 流程图与关键伪码方便理解。
要点先看一眼
- 定义:双亲委派是 Java 的类加载策略:一个类加载器在尝试加载类时,先把请求委派给父加载器,父加载器找不到时再由自己去加载。
- 目的:保证 JVM 核心类(
java.*等)由启动类加载器加载,避免用户类覆盖核心类,保证类型安全与一致性(single source for core libs)。 - 决定类身份:一个类的“身份”由(类的完全限定名 + 它的定义类加载器)共同决定。两个不同加载器加载同名类是不同类型。
- 可以被覆盖:应用服务器 / 插件框架常常打破或调整双亲委派(child-first 或 selective delegation)来实现隔离或热部署。
经典行为(伪码,现代 JDK 风格)
下面是简化后的 ClassLoader loadClass 流程伪码(符合现代 JDK 的设计思想:先查已加载 → 父委派 → 自己加载 → 可选解析)。注意同步细节(并行能力),下面展示 getClassLoadingLock 机制的伪实现:
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// 1. 同步(现代 JVM 用 per-classname lock 支持并行加载)
synchronized (getClassLoadingLock(name)) {
// 2. 已加载?(避免重复加载)
Class<?> c = findLoadedClass(name);
if (c == null) {
// 3. 委派给 parent(parent == null 表示 Bootstrap loader)
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException ignored) {
// 父找不到 -> 由当前 loader 自己尝试
}
// 4. 父也没有 -> 自己 findClass(通常由用户覆盖)
if (c == null) {
c = findClass(name); // read bytes -> defineClass(...)
}
}
// 5. resolve 可选:把符号引用解析为直接引用
if (resolve) resolveClass(c);
return c;
}
}
要点说明:
findLoadedClass(name):检查此加载器是否已加载过该类(不会检查 parent)。parent.loadClass(...):父委派,可传播到 Bootstrap。Bootstrap loader 在 Java 层通常表示为null;由 VM 内部实现findBootstrapClassOrNull。findClass(name):由自定义 ClassLoader 覆盖,负责定位/读字节码并调用defineClass。resolveClass:把常量池中的符号引用解析为直接引用(可延迟,也可以在此处执行)。- 同步:防止并发重复加载;现代 JDK 通过
getClassLoadingLock(name)支持并行 capable loaders(ClassLoader.registerAsParallelCapable()),而不是简单地把synchronized(this)用作锁,从而提升并发性能。
为什么要使用双亲委派?(动机/历史)
- 防止核心类被篡改:例如,
java.lang.String必须只由 Bootstrap loader 提供;双亲委派确保用户定义的同名类不会先被加载。 - 类型一致性:JVM 希望某个核心类在整个运行时只有一个定义源,避免类实现冲突导致不可预期错误。
- 安全:安全管理器/沙箱依赖于核心库由受信任加载器加载。
- 简单性:分层加载器体系使模块化与隔离更容易实现(应用类与平台类明显分离)。
关键细节(经常被误解或导致 bug 的点)
1. Bootstrap loader = null(在 Java 层)
父为 null 表示引导类加载器(由 JVM 实现),它加载平台/核心类(以前是 rt.jar,现在模块系统)。当 parent 为 null 时 ClassLoader 会调用 VM 内部方法来尝试从 bootstrap 类路径加载类。
2. 类的“身份”与 ClassCastException
若 A 类由系统类加载器加载而 B(同名类)由自定义加载器加载,则 A.class 与 B.class 不是同一个类型。常见后果:向方法传递载入自另一个加载器的实例将抛 ClassCastException。
示例:com.example.Plugin$Data 被 host 加载与 plugin 加载两次 -> 不能相互强制转换。
3. findLoadedClass 只查当前加载器的缓存
findLoadedClass 只查“该加载器”是否已经加载过指定类,不会自动检查 parents。loadClass 的父委派先行,因此父加载器加载的类会被返回。
4. 并发加载与 getClassLoadingLock
老版本 JVM 使用同步(this) 导致大量锁竞争;现代 JDK 提供“并行可加载 ClassLoader”机制(ClassLoader.registerAsParallelCapable()),并通过 getClassLoadingLock(name) 为不同类名提供更细粒度锁,从而允许不同线程并行加载不同类。
5. 资源加载也遵循委派(通常)
getResource / getResources 一类的方法也遵循父委派(父先查),但实现细节可以被覆盖且不同容器可能有特殊策略。
常见变体:什么时候不按双亲委派
-
child-first(子优先)/ parent-last:容器或插件框架为保证插件可以使用自己版本的库,先尝试由子加载器加载,再委派父加载器。
- 优点:实现版本隔离、热部署更灵活。
- 缺点:可能会替换核心库,或导致类型冲突(多个同名类存在于不同加载器),需要非常小心。
- 实现方式:覆写
loadClass,先调用findLoadedClass,再尝试findClass,最后委派父。示例如下(简化):
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
// child-first: 先自己找(但保留 java.* 的父优先策略)
if (!name.startsWith("java.")) {
c = findClass(name);
}
} catch (ClassNotFoundException ignored) { }
if (c == null) {
c = super.loadClass(name, false); // 父委派
}
}
if (resolve) resolveClass(c);
return c;
}
}
- selective delegation(选择性委派):很多应用服务器(Tomcat/Jetty)对某些包做父优先或子优先的例外处理(例如确保
javax.servlet.*的 API 由容器提供,但应用的实现类由应用加载器提供)。
与 Thread Context ClassLoader(TCCL)的关系
- TCCL(线程上下文类加载器) 是 JVM 提供的一个 hook:某些库(SPI、JAXB、JNDI、服务加载器
ServiceLoader)会使用Thread.currentThread().getContextClassLoader()来加载用户类或服务实现,这样运行在系统/平台代码中的库可以“回退”到应用类加载器加载应用资源。 - 典型场景:服务发现时(
ServiceLoader.load(...))默认使用 TCCL;所以框架在启动线程前常把 TCCL 设置为 webapp 的类加载器以便框架加载 webapp 的类。 - 风险:如果不正确恢复 TCCL 或在池化线程中不谨慎设置,会导致类/资源被错误加载甚至类加载器泄漏。
类卸载与双亲委派的影响
- 类的卸载通常与定义该类的 ClassLoader 的回收挂钩(只有当加载器可被 GC 回收,且没有遗留静态引用、线程或 JNI 全局引用时,才能卸载)。
- 双亲委派关系能影响 GC 可达性:例如父加载器持有对某些类的引用会阻止子加载器所加载类的卸载;复杂容器环境下容易发生“类加载器泄露”。
调试与排错建议
- ClassCastException 出现时:打印
obj.getClass().getClassLoader()与TargetType.class.getClassLoader(),看是否不同。 - 类重复加载:检查应用是否在多个 classpath / jar 中包含同名类,或是否存在多个 classloader。
- 类泄露:使用 heap dump / MAT 检查是否有静态集合、线程、ThreadLocal、JNI 全局引用引用着 ClassLoader 或 Class 对象。
- 并发类加载问题:查找是否自定义加载器未正确实现并发策略(未使用
getClassLoadingLock等)。
mermaid 流程图(父委派主路径)

总结(快速回顾)
- 双亲委派是 JVM 的默认类加载策略,保证了核心类的一致性与安全。
- Java 层
ClassLoader.loadClass的典型流程是:已加载检查 → 父委派 → 自己查找(findClass/defineClass)→ 可选解析。 - 现代 JDK 支持并行类加载锁(
getClassLoadingLock),以减少锁竞争。 - 可以打破双亲委派(child-first / selective),但要非常小心类身份与安全问题。
- TCCL、ServiceLoader、模块系统、类卸载、类泄漏等与类加载器语义紧密相关,实际工程中需要谨慎设计加载器之间的边界。
本文来自博客园,作者:NeoLshu,转载请注明原文链接:https://www.cnblogs.com/neolshu/p/19120775

浙公网安备 33010602011771号