这个案例能帮你彻底搞懂Java类加载器的双亲委派模型
📚 目录导读
- 双亲委派模型的核心概念
- 为什么需要双亲委派?经典案例切入
- 自定义java.lang.String引发的思考
- Tomcat如何打破双亲委派实现隔离
- 双亲委派模型的工作流程详解
- 高频面试问答录
- 总结与最佳实践
双亲委派模型的核心概念
在Java生态中,类加载器(ClassLoader)负责将字节码加载到JVM内存中,双亲委派模型(Parents Delegation Model)是Java默认采用的类加载策略,其核心思想是:当一个类加载器需要加载某个类时,它首先将加载请求委派给父类加载器,只有当父类加载器无法完成加载时,子加载器才会尝试自行加载。

JVM内置了三个层级的关键类加载器:
- Bootstrap ClassLoader(启动类加载器):加载
$JAVA_HOME/jre/lib/rt.jar等核心库 - Extension ClassLoader(扩展类加载器):加载
$JAVA_HOME/jre/lib/ext目录下的类 - Application ClassLoader(应用类加载器):加载
classpath中用户自定义的类
这种层次结构保证了Java核心API的稳定性和安全性。
为什么需要双亲委派?经典案例切入
核心问题场景
假设没有双亲委派,用户自定义了一个 java.lang.String 类,并在其中添加恶意代码,如果直接由应用类加载器加载,那么核心API将被污染,整个JVM的安全防线瞬间崩塌。
双亲委派的解决方案:当应用类加载器收到加载 java.lang.String 的请求时,它会向上委派给扩展类加载器,扩展类加载器继续向上委派给启动类加载器,启动类加载器在 rt.jar 中找到了标准的 String 类并成功加载,于是用户自定义的恶意 String 永远不会被加载,这就是双亲委派保护Java核心库不被篡改的根本机制。
通俗类比
可以把类加载器想象成一个家族企业:
- 爷爷(Bootstrap)掌握核心技术(核心API)
- 爸爸(Extension)拥有扩展业务
- 儿子(Application)负责外部项目
任何项目需求(类加载请求),都先交给爷爷处理,爷爷能解决就不需要儿子出手;只有爷爷无法处理(找不到类)时,才下放给爸爸,最终落到儿子手里,这种“长老优先”的机制防止了“山寨技术”混入核心业务。
案例一:自定义java.lang.String引发的思考
实验场景
我们尝试编写一个 java.lang.String 类,并尝试通过 Class.forName("java.lang.String") 或直接打印 String.class.getClassLoader() 观察结果。
// 自定义的java.lang.String
package java.lang;
public class String {
static {
System.out.println("恶意String被加载!");
}
}
实际结果
即使在 classpath 中放置了自定义 String,运行后你会发现:
- 没有任何“恶意String被加载”的输出
String.class.getClassLoader()返回null(表示由Bootstrap ClassLoader加载)- 程序正常使用标准String功能
背后的机制
当JVM启动时,Bootstrap ClassLoader已经预加载了核心类,用户自定义的同名包路径类,由于双亲委派机制,永远不会有被应用类加载器加载的机会,JVM会直接使用高优先级的核心类版本。
重要启示
- 无法通过自定义同名类替换JDK核心类(安全屏障)
- 如果需要扩展核心类功能,应使用
-Xbootclasspath/p:参数(不推荐,破坏安全) - 企业实践中,应避免使用
java.*包名自定义类
案例二:Tomcat如何打破双亲委派实现隔离
矛盾点
Tomcat需要为部署在其中的多个Web应用提供互相隔离的类加载环境,如果严格遵循双亲委派,一个应用修改了某个类,会影响其他应用 —— 这显然不符合Servlet规范。
Tomcat的创新方案
Tomcat创建了自定义的 WebappClassLoader,它修改了加载顺序:
- 首先尝试自行加载 Web应用
/WEB-INF/classes和/WEB-INF/lib/*.jar中的类 - 如果找不到,再委派给父类加载器
这就是经典的 “先子后父”模式,严格来说是对双亲委派模型的破坏(违反了自底向上委派的约定),但在Java规范中允许通过 getParent() 设置父加载器并重写 loadClass() 方法实现。
代码层面实现
public class WebappClassLoader extends ClassLoader {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
// 1. 检查是否已经加载
Class<?> clazz = findLoadedClass(name);
if (clazz != null) return clazz;
try {
// 2. 先尝试自行加载(打破委派)
clazz = findClass(name);
if (clazz != null) return clazz;
} catch (ClassNotFoundException e) {
// 忽略
}
// 3. 如果没找到,再委派给父加载器
return super.loadClass(name);
}
}
哪些场景需要打破双亲委派?
| 场景 | 典型方案 | 目的 |
|---|---|---|
| Web应用隔离 | Tomcat WebappClassLoader | 应用间类隔离,互不干扰 |
| 热部署 | OSGi框架、JDK9模块化 | 动态卸载和更新模块 |
| 数据库驱动加载 | 线程上下文类加载器 | 打破SPI加载限制 |
双亲委派模型的工作流程详解
源码级流程解读(基于JDK8的 ClassLoader.loadClass())
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 ClassLoader
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 父加载器抛出异常,说明无法加载
}
if (c == null) {
// 4. 父加载器无法加载,子加载器自己尝试
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
流程图解
客户端(类加载请求)
↓
Application ClassLoader(检查缓存)
↓ 没找到
委派给 Extension ClassLoader(检查缓存)
↓ 没找到
委派给 Bootstrap ClassLoader(检查缓存)
↓ 没找到,且rt.jar中无此类
返回 ClassNotFoundException ← 子加载器逐层尝试
↓
Extension ClassLoader 尝试从 ext 目录加载
↓ 失败
Application ClassLoader 尝试从 classpath 加载
关键知识点
- 唯一性保证:同一个类全限定名在JVM中只能被同一个类加载器加载一次
- 可见性:子加载器可以访问父加载器加载的类,反之不行
- 缓存机制:
findLoadedClass()检查的缓存是类加载器级别的
高频面试问答录
Q1:“如果父加载器加载了某些类,子加载器还能再加载同名类吗?”
A:不能。 双亲委派保证了类加载的唯一性,当父加载器成功加载某个类后,子加载器收到同名请求时,会在第一步 findLoadedClass() 时发现类已被加载(基于类的全限定名+加载器实例的二元组),直接返回父加载器加载的Class对象。
Q2:“破坏双亲委派一定是坏事吗?”
A:不一定。 虽然是默认机制,但有些场景需要主动破坏:
- 应用服务器隔离:Tomcat、Jetty需要为不同Web应用提供独立的类空间
- JDBC驱动加载:SPI机制需要线程上下文类加载器打破层级限制
- 热部署:如Netty、Vert.x等需要动态替换类和JAR包
Q3:“如何查看一个类是由哪个类加载器加载的?”
A: 调用 Class.getClassLoader() 方法:
System.out.println(String.class.getClassLoader()); // null(Bootstrap) System.out.println(ChromeDriver.class.getClassLoader()); // 应用类加载器 System.out.println(com.sun.crypto.provider.SunJCE.class.getClassLoader()); // 扩展类加载器
Q4:“同一个类可以被不同的类加载器加载多次吗?”
A:可以。 每个类加载器有自己的命名空间,例如Tomcat中两个Web应用部署了同名的 com.example.User 类,它们分别由各自的 WebappClassLoader 加载,两者在JVM中是不同的类,无法互相赋值(会抛出 ClassCastException),这正是隔离的原理。
总结与最佳实践
核心价值
双亲委派模型是Java安全体系的重要基石,它通过层级化的委托机制实现了:
- 安全性:核心API免受自定义代码污染
- 稳定性:避免类加载混乱导致的内存泄漏
- 标准化:统一类加载行为,便于JVM优化
实际开发建议
- 不要尝试覆盖JDK核心类:即便在本地开发环境成功,在部署环境也会因双亲委派而失效
- 理解SPI机制:JDBC、Java Naming等SPI接口需要线程上下文类加载器,这是官方认可的打破双亲委派方式
- 容器化部署注意:在Docker/容器环境下,类加载器行为与标准JDK一致,但需注意基础镜像的JDK版本
- 调试技巧:遇到
ClassNotFoundException或NoClassDefFoundError时,首先检查类路径优先级,再考虑双亲委派是否导致类不可见
延伸思考
JDK 9引入的模块化系统(JPMS)虽然改变了类加载的组织方式,但双亲委派的思想仍然存在,模块化进一步细化了类的可见性控制,而底层委派逻辑依然是Bootstrap → Platform → Application的模式。