什么是双亲委派模型?怎么破坏?
一则或许对你有用的小广告
欢迎加入小哈的星球,你将获得:专属的实战项目(4个项目都能学) / 1v1 提问 / 简历修改 / Java 学习路线 / 社群讨论 / 学习打卡 / 每月赠书
《Spring AI 项目实战(问答机器人、RAG 智能客服、联网搜索)》已完结,基于
Spring AI + Spring Boot 3.x + JDK 21...,查看介绍《从零手撸:仿小红书(微服务架构)》 已完结,基于
Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...,查看介绍;演示链接:http://116.62.199.48:7070/《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接:http://116.62.199.48/
新开坑项目:《从零手撸:秒杀系统高并发优化实战》 正在更新中...,查看介绍
截止目前,星球内专栏累计输出 150w+ 字,讲解图 5110+ 张,还在持续爆肝中.. 后续还会上新更多项目,已有 4700+ 小伙伴加入学习,欢迎点击围观
面试考察点
-
类加载机制理解:面试官不仅仅是想听你背出 "双亲委派" 四个字,更是想看你能不能把类加载器的层次结构、委派流程、以及这么设计的原因讲清楚。
-
安全性认知:双亲委派模型本质上是一种安全机制。如果你能说出 "防止
java.lang.Object被恶意篡改" 这种例子,说明你理解了它存在的意义,而不只是记了个流程。 -
打破常规的思维:问 "怎么破坏" 其实是在考察你的实战经验和源码阅读能力。JDK 自身就破坏过双亲委派,如果你能举出具体例子(SPI、Tomcat、OSGi),面试官会觉得你研究得够深。
核心答案
双亲委派模型 是 Java 类加载器的核心工作机制:当一个类加载器收到加载请求时,不会自己先去加载,而是把请求 委派给父加载器 处理。只有当父加载器反馈自己无法完成加载时,子加载器才会尝试自己去加载。
上图展示了类加载器的层次关系。整体加载流程如下:
- 最顶层 是
Bootstrap ClassLoader(启动类加载器),由 C++ 实现,负责加载<JAVA_HOME>/lib目录下的核心类库(如rt.jar),它是所有类加载器的 "祖宗" - 第二层 是
Extension ClassLoader(扩展类加载器),负责加载<JAVA_HOME>/lib/ext目录下的扩展类库 - 第三层 是
Application ClassLoader(应用类加载器),负责加载用户 classpath 下的类,也是我们日常开发中用的最多的 - 最底层 是各种自定义
ClassLoader,开发者可以按需实现
关键在于箭头方向——加载请求从下往上委派,实际加载从上往下尝试。子加载器先把活交给父加载器,父加载器搞不定才轮到自己。
深度解析
一、双亲委派的工作流程
假设你的代码里写了 new String("hello"),JVM 需要加载 java.lang.String 类,流程是这样的:
对应的源码在 ClassLoader.loadClass() 方法中,核心逻辑非常简洁:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 1. 先检查是否已经加载过
Class<?> c = findLoadedClass(name);
if (c == null) {
// 2. 没加载过,先委派给父加载器
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 父加载器为 null,说明到了最顶层,委派给 Bootstrap
c = findBootstrapClassOrNull(name);
}
// 3. 父加载器也加载不了,自己尝试加载
if (c == null) {
c = findClass(name);
}
}
return c;
}
}
流程三步走:
- 第一步:检查本加载器是否已经加载过该类,有就直接返回
- 第二步:没有的话,调用
parent.loadClass()委派给父加载器(这就是 "双亲委派" 的核心) - 第三步:父加载器也加载不了(返回
null),才调用自己的findClass()去加载
二、为什么要用双亲委派?
两个核心原因:
1. 保证类的唯一性
不管哪个类加载器加载 java.lang.Object,最终都会委派到 Bootstrap ClassLoader,保证全 JVM 中只有一个 Object 类。如果没有双亲委派,你自己写一个 java.lang.Object 就能把核心类给替换了,那 Java 的安全性就荡然无存了。
2. 保证安全性
防止恶意替换核心类。即使你自定义了一个 java.lang.Hack 类,由于双亲委派机制,Bootstrap ClassLoader 会先尝试加载,发现核心库没有这个类才会轮到下面的加载器——而且对于 java.lang 包,JVM 还有额外的包密封检查,直接拒绝。
三、怎么破坏双亲委派模型?
双亲委派不是铁律,JDK 自身就有三次 "破坏":
第一次:JDK 1.2 之前(历史遗留)
双亲委派模型是在 JDK 1.2 才引入的,但 ClassLoader 类从 JDK 1.0 就存在了。之前用户自定义类加载器只需要重写 loadClass() 方法,而双亲委派的逻辑也在 loadClass() 里——你重写了它,委派机制就被覆盖了。
JDK 1.2 的解决办法是新增了一个 findClass() 方法,引导用户重写 findClass() 而不是 loadClass(),这样 loadClass() 里的双亲委派逻辑就能保留下来。严格来说这不叫 "破坏",而是 "打补丁"。
第二次:SPI 机制(JNDI、JDBC 等)
这是最经典的破坏案例,面试必问。
SPI(Service Provider Interface)的痛点在于:接口在核心库(rt.jar)中,由 Bootstrap ClassLoader 加载;但实现类是第三方 jar 包,在 classpath 下,只能由 Application ClassLoader 加载。
问题来了——Bootstrap ClassLoader 是最顶层的加载器,它 看不到 classpath 下的类,也没法向下委派给子加载器。双亲委派是单向的,只能往上走。
JDK 的解决方案是 线程上下文类加载器(Thread Context ClassLoader):
// DriverManager 在 rt.jar 中,由 Bootstrap ClassLoader 加载
// 但它需要加载 classpath 下的数据库驱动
// 通过线程上下文类加载器,拿到了 Application ClassLoader
ClassLoader cl = Thread.currentThread().getContextClassLoader();
// 用 Application ClassLoader 去加载驱动实现类
Class<?> driverClass = cl.loadClass("com.mysql.cj.jdbc.Driver");
说白了就是:接口让父加载器加载,实现类通过线程上下文类加载器 "偷偷" 拿到子加载器来加载,绕过了双亲委派的限制。这个设计确实有点 hack,但在当时的架构下也算是一种优雅的妥协。
第三次:OSGi / 模块化热部署
OSGi 框架(比如 Eclipse 的插件机制)实现了自己的类加载器网状结构,每个 Bundle 有独立的类加载器,类加载不再是简单的树形双亲委派,而是变成了更复杂的图结构:
这种设计可以实现模块的 热部署 和 热替换,每个模块的类互相隔离,也可以按需通信。
Tomcat 也破坏了双亲委派
Tomcat 作为 Web 容器,每个 Web 应用都要做到类隔离——你部署了两个应用,一个用 Spring 4,一个用 Spring 5,它们加载的 Spring 类不能冲突。所以 Tomcat 的 WebAppClassLoader 会 优先加载自己 Web 应用目录下的类,而不是先委派给父加载器,这就打破了双亲委派的规则。
不过 Tomcat 也不是完全不用双亲委派——对于 Java 核心类(java.lang.* 等),它还是会走双亲委派的,保证核心类的安全性。
面试高频追问
-
自定义类加载器需要重写哪个方法?
重写
findClass()方法。别重写loadClass(),那样会破坏双亲委派机制。loadClass()内部已经实现了委派逻辑,最终会调到你重写的findClass()上。 -
为什么 Tomcat 要破坏双亲委派?
两个原因:一是 Web 应用之间的类隔离(不同应用可能依赖同一个库的不同版本);二是 JSP 的热加载,修改 JSP 后不需要重启整个容器。这两个需求在标准双亲委派下都无法实现。
-
双亲委派能保证一个类在 JVM 中只被加载一次吗?
不一定。不同的类加载器加载同一个
.class文件,得到的Class对象是不同的。即使用==比较会返回false。所以 "类的唯一性" 是在 同一个类加载器命名空间 下才有保证的。
常见面试变体
- "为什么 Java 要设计双亲委派模型?"
- "说几个双亲委派被破坏的实际例子"
- "Tomcat 的类加载器是怎么设计的?"
- "SPI 机制为什么要用线程上下文类加载器?"
记忆口诀
双亲委派:子找父,父找爷,爷干不了才回子——往上委派,往下兜底。
三次破坏:
- 第一次:历史遗留,重写
loadClass()→ 引导用findClass() - 第二次:SPI 打不通 → 线程上下文类加载器来救场
- 第三次:OSGi/Tomcat 要隔离 → 直接搞自己的类加载器
总结
双亲委派模型的核心就是 "往上委派,往下兜底"——先让父加载器去加载,搞不定自己再上。它能保证核心类的安全性和唯一性,但在 SPI、Tomcat、OSGi 等场景下确实有局限,所以 JDK 自身和各类框架都有打破它的做法。面试时把委派流程讲清楚,再举出 SPI 和 Tomcat 两个破坏例子,基本就是满分。
