JVM 双亲委派模型(Parent Delegation)详解
关键词:ClassLoader、类的唯一性、安全、隔离、可替换/可插拔
本文讲清楚三件事:
- 双亲委派到底是什么规则(不是“父类加载器加载”这么一句话)。
- 为什么 JVM 要这么设计(安全 + 统一 + 隔离)。
- 现实里怎么被打破(Tomcat / SPI / OSGi / 热部署等),以及你排查 ClassLoader 问题的套路。
1. 类加载器体系:谁负责加载谁?
JVM 里“把 .class 变成 Class<?>”的东西叫 类加载器(ClassLoader)。常见三层:
-
Bootstrap ClassLoader(启动类加载器)
- 用 C/C++ 实现(HotSpot 里不是 Java 对象)
- 负责加载 JDK 核心类:
java.*、javax.*(部分)、jdk.*等 - 类路径:典型是
jre/lib、jmods(Java 9+)
-
Platform ClassLoader(平台类加载器)(Java 9+)
- 负责加载平台模块(以前很多
rt.jar里拆出来的东西)
- 负责加载平台模块(以前很多
-
Application ClassLoader(应用类加载器)
- 负责加载你应用的 classpath(
-cp/CLASSPATH)下的类
- 负责加载你应用的 classpath(
另外还有:
- 自定义 ClassLoader:Tomcat、Spring Boot、OSGi、插件框架、脚本引擎等经常自己搞一套。
你可以在代码里看看当前环境:
public class LoaderDemo {
public static void main(String[] args) {
ClassLoader cl = LoaderDemo.class.getClassLoader();
System.out.println("App: " + cl);
System.out.println("Parent: " + cl.getParent());
System.out.println("Grandparent: " + cl.getParent().getParent()); // 通常为 null => Bootstrap
System.out.println("String loader: " + String.class.getClassLoader()); // null => Bootstrap
}
}
2. 双亲委派模型:规则是什么?
一句话版本:“一个类加载请求先交给父加载器,父加载器不行才自己来。”
更精确一点(这个才是真正的“模型”):
- 先检查缓存:这个 ClassLoader 是否已经加载过这个类(避免重复定义)。
- 如果有父加载器:把加载请求委派给父加载器(递归向上)。
- 父加载器也加载不了(抛
ClassNotFoundException):- 子加载器才会尝试调用自己的
findClass()来加载。
- 子加载器才会尝试调用自己的
伪代码(接近 JDK ClassLoader#loadClass 的逻辑):
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// 1) 已加载?
Class<?> c = findLoadedClass(name);
// 2) 没加载过,先委派父加载器
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 父加载器加载不了,走 3)
}
// 3) 父加载器不行,我自己找
if (c == null) {
c = findClass(name);
}
}
if (resolve) resolveClass(c);
return c;
}
注意:这不是“必须”的 JVM 规则,而是“JDK 默认 ClassLoader 的实现策略”
- JVM 规范并不强制你必须委派父加载器。
- 但 JDK 提供的
ClassLoader默认实现就是这个策略。 - 你可以通过重写
loadClass()来改变(这就是“打破双亲委派”的入口)。
3. 为什么要双亲委派?(设计动机)
3.1 安全:防止核心类被替换/伪造
如果没有委派机制,你完全可以在应用 classpath 里放一个 java.lang.String,让 JVM 加载你的假 String —— 这会直接把安全边界打穿。
有了双亲委派:
- 加载
java.lang.String时,应用类加载器先问父加载器 - 父加载器一路到 Bootstrap,Bootstrap 先把真正的
java.lang.String加载了 - 你的“假 String”根本没机会出场
3.2 一致性:同名核心类全 JVM 统一
比如 java.util.List 在整个 JVM 里必须是同一个 Class 对象,不然各种库之间根本没法协作。
3.3 隔离:不同层次的类彼此隔离又能共享基础能力
- 上层(应用)依赖下层(JDK 核心)是常态
- 下层不应该“反向”依赖上层
- 委派天然保证“基础设施”稳定、“业务代码”可变
4. 类的唯一性:你踩坑多半都在这
类在 JVM 中的唯一性不是只看“类的全限定名”,而是:
Class = (ClassLoader, FullyQualifiedName)
也就是说:
- 同名同包的类,只要由不同 ClassLoader加载,就是两个完全不同的类型。
- 你会看到经典报错:
java.lang.ClassCastException: A cannot be cast to A- 或者
NoSuchMethodError/LinkageError
例子(两个不同 Loader 加载同一个字节码):
Class<?> c1 = loader1.loadClass("com.demo.User");
Class<?> c2 = loader2.loadClass("com.demo.User");
System.out.println(c1 == c2); // false
System.out.println(c1.getClassLoader());
System.out.println(c2.getClassLoader());
5. 什么时候会“打破双亲委派”?(现实世界)
双亲委派是默认策略,但很多框架/容器为了功能必须“反着来”或“改一点”。
5.1 Tomcat:Web 应用隔离(经典“WebAppClassLoader”)
需求:
- 多个 webapp 运行在同一个 JVM
- 每个应用有自己的
WEB-INF/lib - 不同应用可以用不同版本的同一个库
- 但还得共享 servlet 规范 API
做法(简化):
- 对大多数业务类:优先自己加载(child-first),保证隔离
- 对 servlet/jsp 等容器 API:仍然委派父加载器,保证共享
这就是:部分 child-first,部分 parent-first。
5.2 SPI(Service Provider Interface):父加载器反向用子加载器
典型:JDBC 驱动(历史上尤为经典)
java.sql.DriverManager是 Bootstrap 或平台类加载器加载的(在“父”里)- 驱动实现类(
com.mysql.cj.jdbc.Driver)在应用 classpath(在“子”里)
如果严格双亲委派:
- 父加载器看不到子加载器的类
DriverManager怎么加载驱动实现?
解决:线程上下文类加载器(TCCL, Thread Context ClassLoader)
- 让“父层的代码”临时使用“子层的加载器”去加载 SPI 实现
获取方式:
ClassLoader tccl = Thread.currentThread().getContextClassLoader();
很多框架(JNDI、JAXP、日志门面等)都用过这个套路。
5.3 OSGi / 插件化:多版本共存、按需可卸载
OSGi 直接把“类可见性”做成模块依赖图:
- A bundle 看不见 B bundle 的类,除非显式导入/导出
- 版本也能隔离(同名包多版本共存)
这本质上就是一套更复杂的“类加载路由”。
5.4 热部署/热加载:替换 class
比如某些脚本引擎、热更新框架:
- 通过“新建一个 ClassLoader”加载新版本 class
- 旧 ClassLoader 及其 Class 变成“不可达”后才能被 GC 回收(前提是没有泄漏引用)
6. 怎么“打破”双亲委派?(技术手段)
双亲委派之所以能被破坏,是因为你可以重写 loadClass():
- parent-first(默认):先委派父加载器,再自己
- child-first:先自己
findClass(),找不到再委派父加载器
child-first 示例(非常简化,仅示意):
public class ChildFirstClassLoader extends ClassLoader {
public ChildFirstClassLoader(ClassLoader parent) {
super(parent);
}
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
// 1) 先自己找(打破委派)
c = findClass(name);
} catch (ClassNotFoundException ignored) {
// 2) 自己找不到再问父
c = super.loadClass(name, false);
}
}
if (resolve) resolveClass(c);
return c;
}
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 从自定义路径读取字节码,然后 defineClass(...)
throw new ClassNotFoundException(name);
}
}
现实里不会这么“裸写”,一般会配合 jar/目录扫描、缓存、黑白名单(哪些包 parent-first,哪些包 child-first)。
7. 排查 ClassLoader 问题的实战套路
7.1 先看“谁加载的”
任何诡异的类型转换/方法找不到,第一件事:
System.out.println(SomeClass.class.getClassLoader());
System.out.println(SomeInterface.class.getClassLoader());
如果你看到:
- 接口由 A loader 加载
- 实现类由 B loader 加载
那基本就能解释很多ClassCastException。
7.2 典型症状 -> 典型原因
-
ClassCastException: X cannot be cast to X- 99% 是 不同 ClassLoader 加载了同名类
-
NoSuchMethodError/NoSuchFieldError- 常见是 依赖冲突:编译时用的版本和运行时实际加载版本不一致
- 也可能是类加载顺序导致“加载到了旧版本”
-
LinkageError: loader constraint violation- 两个 loader 对同一个符号引用约束不一致(复杂但本质还是类型不统一)
7.3 Tomcat / Spring Boot 常见坑
- web 容器里重复放了同一个 jar(
WEB-INF/lib+ 容器 shared lib) - fat jar / 多层 classpath 导致版本 shadowing
- 日志框架(slf4j / logback / log4j2)多实现共存导致冲突
8. 一张图把双亲委派讲明白
loadClass("com.demo.Foo")
|
+----------v-----------+
| AppClassLoader |
+----------+-----------+
|
委派给 parent
|
+----------v-----------+
| PlatformClassLoader |
+----------+-----------+
|
委派给 parent
|
+----------v-----------+
| BootstrapClassLoader |
+----------+-----------+
|
找到/加载? yes -> 返回
|
no (CNF)
|
回到子加载器 findClass()
9. 记住这几句就够用了
- 默认就是 parent-first:先父后子。
- 核心目的:安全 + 一致性。
- 类唯一性:
(ClassLoader, 类名),不是只有类名。 - 打破委派的原因:隔离、多版本、插件化、SPI。
- 排查第一步:打印
getClassLoader(),看是不是“同名不同 loader”。
10. 延伸阅读建议
想深入的话,可以按这个顺序看:
java.lang.ClassLoader源码:loadClass / findClass / defineClass- TCCL:
Thread#getContextClassLoader - Tomcat ClassLoader 架构(Common / Catalina / Shared / WebApp)
1216

被折叠的 条评论
为什么被折叠?



