什么是双亲委派?怎么破坏?
一则或许对你有用的小广告
欢迎 加入小哈的星球 ,你将获得: 专属的项目实战(已更新的所有项目都能学习) / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论
- 新开坑项目: 《Spring AI 项目实战(问答机器人、RAG 增强检索、联网搜索)》 正在持续爆肝中,基于
Spring AI + Spring Boot3.x + JDK 21..., 点击查看; - 《从零手撸:仿小红书(微服务架构)》 已完结,基于
Spring Cloud Alibaba + Spring Boot3.x + JDK 17..., 点击查看项目介绍; 演示链接: http://116.62.199.48:7070/; - 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/
面试考察点
- 对 JVM 类加载机制的理解:面试官想考察你是否了解类加载器的层次结构,以及双亲委派模型是如何工作的。
- 为何要设计双亲委派:不仅仅是背诵定义,更重要的是理解它的设计初衷——保证 Java 核心类库的安全性和唯一性,避免用户自定义类覆盖核心类。
- 破坏双亲委派的场景与手段:考察你是否在实际开发或框架设计中遇到过需要打破常规的场景,比如 Tomcat 如何隔离 Web 应用、JDBC 如何加载驱动等。
- 源码级认知:通过
loadClass方法的实现逻辑,判断你是否读过 JVM 或 ClassLoader 相关的源码。 - 扩展性与灵活性的权衡:理解双亲委派带来的好处,同时也知道在某些情况下它会成为瓶颈,需要破坏它来获得更大的灵活性。
核心答案
双亲委派模型 是 Java 类加载器在加载类时采用的一种委托机制。当一个类加载器收到加载类的请求时,它首先不会自己去尝试加载这个类,而是将这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此。因此,所有的加载请求最终都应该传送到顶层的启动类加载器中。只有当父类加载器反馈自己无法加载这个类时(它的搜索范围中没有找到所需的类),子类加载器才会尝试自己去加载。
这种机制保证了 Java 程序运行的安全性和稳定性。例如,我们写的 java.lang.Object 类,无论在哪一个类加载器中,最终都会委托给启动类加载器加载,从而保证了 JVM 中只有唯一的一份 Object 类。
破坏双亲委派 通常指不遵循这个委托流程,让类加载器尝试自己先加载类,或者在特定场景下绕过父加载器直接加载。常见的破坏方式包括:
- 自定义 ClassLoader 并重写
loadClass方法,不遵守先委托父类的逻辑。 - 利用线程上下文类加载器,将父加载器无法加载的类交给子加载器加载(典型应用如 JDBC)。
- OSGi 或模块化框架 为了实现模块间代码隔离,完全颠覆了双亲委派的树形结构,采用网状结构。
深度解析
原理/机制
在 JVM 中,类加载器主要有三层:
- 启动类加载器(Bootstrap ClassLoader):C++ 实现,负责加载
<JAVA_HOME>/lib目录下的核心类库。 - 扩展类加载器(Extension ClassLoader):Java 实现,负责加载
<JAVA_HOME>/lib/ext目录下的类库(JDK 9 后被平台类加载器取代)。 - 应用程序类加载器(Application ClassLoader):负责加载
classpath上的类,也是我们代码中默认的上下文类加载器。
双亲委派的核心代码在 java.lang.ClassLoader 的 loadClass 方法中:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先,检查类是否已经被加载过
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
// 如果有父加载器,则委托父加载器加载
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 如果没有父加载器,说明是顶层,调用启动类加载器
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 父类加载器无法加载,抛出异常说明
}
if (c == null) {
// 如果父类加载器没加载到,再调用自己的 findClass 去加载
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
可见,标准的实现总是先看父类加载器能不能加载,最后才轮到自己。
如何破坏双亲委派?
1. 重写 loadClass 方法,改变委托顺序
最常见的破坏方式就是直接覆盖 loadClass,把查找顺序反过来,或者完全自定义规则。
public class MyClassLoader extends ClassLoader {
@Override
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
// 先自己尝试加载
synchronized (getClassLoadingLock(name)) {
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
c = findClass(name); // 先用自己的 findClass
} catch (ClassNotFoundException e) {
// 自己加载失败,再委托给父类
c = super.loadClass(name, resolve);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 实现自己的类查找逻辑,比如从指定目录读取 .class 文件
// ...
}
}
这种自定义加载器在 Tomcat 等 Web 容器中很常见,每个 Web 应用都有自己的类加载器,优先加载 /WEB-INF/classes 下的类,实现了应用之间的隔离。
2. 线程上下文类加载器
这是 JDK 1.2 引入的一种“破坏”手段,用于解决 Java 核心 SPI(Service Provider Interface)的加载问题。比如 JDBC,java.sql.DriverManager 是由启动类加载器加载的,但它需要加载厂商提供的具体驱动实现(如 com.mysql.jdbc.Driver),这些实现位于 classpath 下,启动类加载器无法加载。为了解决这个矛盾,引入了线程上下文类加载器,让核心类库可以通过这个加载器去加载外部实现类。
// 在 DriverManager 中,通过线程上下文类加载器加载驱动
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
// 内部会调用 Thread.currentThread().getContextClassLoader()
Thread.currentThread().getContextClassLoader() 默认返回的是应用程序类加载器,可以加载 classpath 下的类。这样,启动类加载器就“逆向”使用了子加载器来加载类,打破了双亲委派的层次性。
3. 热部署与模块化框架
像 OSGi 这样的框架,为了实现模块的热插拔,每个模块都有自己的类加载器,模块之间可以互相引用,形成了网状结构,完全脱离了双亲委派的树形结构。JDK 9 的模块化系统(Project Jigsaw)虽然保留了层次化,但也允许模块间更灵活的依赖。
对比分析
| 特性 | 双亲委派模型 | 破坏双亲委派(以 Tomcat 为例) |
|---|---|---|
| 加载顺序 | 先父后子 | 先子后父 |
| 类唯一性 | 保证核心类库一致,避免冲突 | 实现应用隔离,同一个类可被不同加载器加载多次 |
| 安全性 | 高,防止恶意代码覆盖核心 API | 需要谨慎处理,否则可能引发 LinkageError |
| 适用场景 | 标准 Java 应用 | Web 容器、热部署、OSGi 等 |
最佳实践
- 尽量遵循双亲委派:除非有明确需求(如隔离、热部署),否则不要随意破坏。破坏后需要自己处理类的唯一性和垃圾回收。
- 理解上下文类加载器的用途:编写框架或 SPI 实现时,正确使用
Thread.currentThread().getContextClassLoader()可以避免类加载问题。 - 注意 JDK 版本变化:JDK 9 之后移除了扩展类加载器,引入了平台类加载器,层次略有变化,但双亲委派的理念没变。
- 避免在并发环境下自定义类加载器:类加载器的
loadClass方法默认是同步的,如果自定义实现破坏了同步,可能导致同一个类被加载多次。
常见误区
- 误区:认为双亲委派模型中,所有类加载器都有一个父加载器。
- 正解:启动类加载器没有父加载器,它的
parent为null。
- 正解:启动类加载器没有父加载器,它的
- 误区:认为重写
findClass就是破坏了双亲委派。- 正解:
findClass是loadClass中最后调用的方法,用于自定义类加载逻辑,但委托顺序没有变,所以并没有破坏双亲委派。只有重写loadClass并改变委托顺序才算破坏。
- 正解:
- 误区:认为线程上下文类加载器是一个特殊的加载器。
- 正解:它只是
Thread类中的一个属性,用来保存一个ClassLoader引用,默认是应用程序类加载器。它本身并不是一种新的加载器类型。
- 正解:它只是
总结
双亲委派模型是 Java 类加载器的核心安全机制,通过层层委托确保核心类库的一致性和安全性;而破坏它则是为了解决特定场景下的类隔离、SPI 加载等问题,需要深入理解类加载原理才能正确运用。