Java双亲委派模型深度解析
讲解Java的双亲委派模型:类加载请求会逐级向上委派,以保证核心库安全并避免类的重复加载。
我们来深入浅出地讲解一下Java的双亲委派模型(Parents Delegation Model)。
这是一个非常核心且重要的JVM概念,理解它有助于你明白Java类是如何被加载和隔离的,也能帮你解决很多类似“jar包冲突”的问题。
1. 什么是双亲委派模型?(What)
一句话概括:当一个类加载器(ClassLoader)收到加载类的请求时,它不会自己先去尝试加载,而是先把这个请求委派给它的父类加载器去完成。每一层的类加载器都是如此,因此所有的加载请求最终都会传送到顶层的启动类加载器(Bootstrap ClassLoader)。只有当父加载器无法完成这个加载请求(在它的搜索范围内没有找到所需的类)时,子加载器才会自己去尝试加载。
一个生活化的比喻:
想象一个富三代想买一个玩具。
- 他不会自己直接去店里买,而是先问他爸爸:“爸,你有这个玩具吗?”
- 他爸爸也不会自己去找,而是先问他爷爷:“爸,你有这个玩具吗?”
- 爷爷是家族的创始人,他先在自己的宝库里找。
- 如果找到了,就直接拿出来给孙子。交易结束。
- 如果没找到,他会告诉儿子:“我没有。”
- 这时,儿子才会在自己的仓库里找。
- 如果找到了,就拿给儿子。
- 如果还没找到,他会告诉儿子:“我也没有。”
- 最后,这个富三代才会自己去玩具店里买。如果连玩具店都买不到,那就真的没有了(抛出
ClassNotFoundException)。
在这个比喻中:
- 爷爷 -> 启动类加载器 (Bootstrap ClassLoader)
- 爸爸 -> 扩展类加载器 (Extension ClassLoader / Platform ClassLoader)
- 富三代 -> 应用程序类加载器 (Application ClassLoader)
- 玩具 -> 需要加载的类 (
.class文件)
2. 为什么需要双亲委派模型?(Why)
这种设计主要有两个目的:
a. 避免类的重复加载
一个类,由不同的类加载器加载,在JVM中会被认为是两个完全不同的类。例如,如果你自己写了一个 java.lang.String 类,并用两个不同的加载器加载了系统的String类和你自己写的String类,那么JVM内部就会存在两个String类的实例。
双亲委派模型确保了一个类只会被加载一次。因为所有的加载请求都会最终汇集到顶层的加载器,比如 java.lang.Object,它最终一定是由启动类加载器(Bootstrap ClassLoader)加载的,这就保证了任何地方引用到的 Object 类都是同一个。
b. 保证Java核心库的安全性
这是最重要的原因。如果没有双亲委派,那么任何人都可以编写一个自己的 java.lang.String 类,并在其中植入恶意代码。当JVM需要加载 String 类时,如果恰好加载了你写的这个恶意版本,那么整个Java应用的安全体系都会被摧毁。
有了双亲委派模型:
- JVM要加载
java.lang.String。 - 请求被发送到应用程序类加载器。
- 它委派给父类(扩展类加载器)。
- 扩展类加载器再委派给父类(启动类加载器)。
- 启动类加载器在自己的搜索路径(如
rt.jar)中找到了官方的java.lang.String,并加载它。 - 加载成功后,这个类被返回。你编写的那个恶意
String类根本没有机会被加载。
3. 双亲委派模型是如何工作的?(How)
这需要先了解Java中的几种主要类加载器:
启动类加载器 (Bootstrap ClassLoader)
- 由C++实现,是JVM自身的一部分。
- 负责加载Java最核心的类库,如
<JAVA_HOME>/lib目录下的rt.jar(Java 8及以前)或java.base模块(Java 9及以后)。 - 它没有父加载器,是所有加载器的“祖宗”。
扩展类加载器 (Extension ClassLoader) - 在Java 9中被 平台类加载器 (Platform ClassLoader) 取代。
- 由Java实现。
- 负责加载
<JAVA_HOME>/lib/ext目录下的,或者被java.ext.dirs系统变量所指定的路径中的所有类库。 - 它的父加载器是启动类加载器。
应用程序类加载器 (Application ClassLoader)
- 也叫系统类加载器(System ClassLoader)。
- 由Java实现。
- 负责加载用户类路径(Classpath)上所指定的类库。我们自己写的Java代码,默认就是由它来加载的。
- 它的父加载器是扩展类加载器。
自定义类加载器 (Custom ClassLoader)
- 开发者可以根据自己的需求编写,可以用来实现类的热部署、代码加密等。
- 它的父加载器通常是应用程序类加载器。
加载流程(源码解读)
这个模型的实现核心在 java.lang.ClassLoader 的 loadClass() 方法中。下面是简化后的伪代码:
public Class<?> loadClass(String name) throws ClassNotFoundException {
// 1. 检查这个类是否已经被加载过了
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
// 2. 如果父加载器不为null,就委派给父加载器去加载
if (parent != null) {
c = parent.loadClass(name);
} else {
// 如果父加载器为null(说明父加载器是Bootstrap ClassLoader)
// 委派给Bootstrap ClassLoader去加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 3. 如果父加载器和启动类加载器都找不到这个类
// 此时会捕获异常,但什么都不做,流程继续向下
}
if (c == null) {
// 4. 如果到这里c还是null,说明所有父加载器都加载失败了
// 此时,才轮到自己调用 findClass() 方法进行加载
c = findClass(name);
}
}
return c;
}
关键点:
loadClass():负责整个委派逻辑。findClass():负责实际的类加载(从文件、网络等地方读取字节码,并定义为Class对象)。如果你要自定义类加载器,推荐重写findClass()方法,而不是loadClass()方法,这样可以保留双亲委派的机制。
4. 如何打破双亲委派模型?
虽然双亲委派模型很优秀,但在某些场景下,它并不适用。打破它也是Java生态中常见的操作。
为什么要打破?
一个典型的例子是 Tomcat。Tomcat是一个Web容器,它可能需要部署多个独立的Web应用。
- 应用A可能依赖
library-v1.0.jar。 - 应用B可能依赖
library-v2.0.jar。
如果都使用应用程序类加载器,根据双亲委派,只有一个版本的library会被加载,这会导致另一个应用出错。所以,Tomcat需要实现应用之间的类库隔离。
Tomcat的实现方式:
Tomcat为每个Web应用创建了一个专门的类加载器(WebAppClassLoader)。这个加载器重写了 loadClass() 方法,其逻辑与双亲委派相反:
- 先在自己的仓库(
WEB-INF/classes和WEB-INF/lib)中查找类。 - 如果找到了,就自己加载,不再向上委派。
- 如果自己找不到,才委派给父加载器(Application ClassLoader等)去加载。
这样就保证了每个Web应用使用的类库都是自己私有的,实现了隔离。
其他例子还包括JNDI、OSGi等技术,它们都因自身需要而对双亲委派模型做了调整。
总结
| 特性 | 描述 |
|---|---|
| 核心思想 | 向上委派,向下加载。先问父亲,不行再自己上。 |
| 加载器层次 | Bootstrap -> Platform(Extension) -> Application -> Custom |
| 主要优点 | 安全性(保护核心库)、唯一性(避免重复加载) |
| 实现代码 | ClassLoader.loadClass() 方法 |
| 自定义方式 | 推荐重写 findClass(),保留委派逻辑 |
| 打破场景 | 需要实现隔离和热部署的场景,如Tomcat、OSGi |