Java 的双亲委派模型(Parent Delegation Model)
文章目录
- Java 的双亲委派模型(Parent Delegation Model)
- 一、什么是双亲委派机制?
- 二、为什么要用这种机制?(优点)
- 三、类加载器的层次结构(谁是谁的“双亲”?)
- 四、工作流程(它是如何工作的?)
- 五、如何打破双亲委派?
- 总结
Java 的双亲委派模型(Parent Delegation Model)
一、什么是双亲委派机制?
双亲委派模型是 Java 类加载器(ClassLoader)在加载一个类时所采用的一种工作模式。它的核心思想可以概括为:
当一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成。每一层的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器(Bootstrap ClassLoader)中。只有当父类加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
简单来说,就是 “先让爸爸来,爸爸不行我再上”。
二、为什么要用这种机制?(优点)
双亲委派机制绝不是随意设计的,它解决了几个关键问题:
-
避免类的重复加载(确保唯一性)
通过委派链,一个类会被尽可能高地由某个父加载器加载。这确保了在整个 JVM 中,同一个类(由类加载器实例 + 类的全限定名
共同标识)只会被加载一次,从而避免了多份相同的字节码在内存中同时存在。 -
保护程序安全,防止核心API被篡改(沙箱安全机制)
这是最重要的原因。比如你自己写了一个java.lang.Object
类,如果没有双亲委派,这个类可能会被应用程序类加载器加载,从而覆盖掉 Java 核心库中的Object
类,这将极其危险。
有了双亲委派,你的java.lang.Object
加载请求会一路向上委派给启动类加载器。启动类加载器在它的路径(rt.jar
等)下找到了真正的Object
类并加载它,于是你的那个“恶意”的Object
类就根本没有机会被加载。这保证了 Java 核心库的类型安全。 -
保证程序的稳定和有序
它确立了一套带有优先级的层次关系,使得类的加载变得非常有序。
三、类加载器的层次结构(谁是谁的“双亲”?)
Java 中的类加载器主要分为以下三层(以 Java 8 为例),它们之间不是继承关系,而是组合关系(子加载器中有个parent
字段指向父加载器):
-
Bootstrap ClassLoader(启动类加载器)
- 最高层级,由 C++ 实现,是 JVM 的一部分。
- 负责加载 Java 的核心库(
JAVA_HOME/jre/lib/rt.jar
,resources.jar
等),如java.lang.*
,java.util.*
等。 - 它是所有其他类加载器的“祖先”(它是
ExtClassLoader
的父加载器)。
-
Extension ClassLoader(扩展类加载器)
- 由 Java 实现,是
sun.misc.Launcher$ExtClassLoader
类。 - 负责加载 Java 的扩展库(
JAVA_HOME/jre/lib/ext
目录,或者由java.ext.dirs
系统变量指定的路径中的所有类库)。
- 由 Java 实现,是
-
Application ClassLoader(应用程序类加载器/系统类加载器)
- 由 Java 实现,是
sun.misc.Launcher$AppClassLoader
类。 - 负责加载用户类路径(ClassPath)上所指定的类库。
- 一般情况下,我们程序中默认的类加载器就是它。可以通过
ClassLoader.getSystemClassLoader()
获取。
- 由 Java 实现,是
-
自定义类加载器(Custom ClassLoader)
- 用户可以自己定义类加载器,继承自
java.lang.ClassLoader
,来实现特殊的加载需求(如从网络、加密文件中加载)。
- 用户可以自己定义类加载器,继承自
关系图:
Bootstrap ClassLoader (C++实现,无Java对象)↑Extension ClassLoader (sun.misc.Launcher$ExtClassLoader)↑Application ClassLoader (sun.misc.Launcher$AppClassLoader)↑自定义类加载器 (MyClassLoader) -> 自定义类加载器2...
注意:这里的“父”子关系是组合关系,不是继承关系。
四、工作流程(它是如何工作的?)
双亲委派的工作流程体现在ClassLoader.loadClass(String name, boolean resolve)
方法的默认实现中:
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),则委派给Bootstrap加载c = findBootstrapClassOrNull(name);}} catch (ClassNotFoundException e) {// 父加载器抛出异常,表示无法完成加载}// 4. 如果父加载器(及以上的所有父加载器)都无法完成加载if (c == null) {// 5. 那么才调用自己的findClass方法去尝试加载c = findClass(name);}}if (resolve) {resolveClass(c);}return c;}
}
流程步骤拆解:
假设我们要用AppClassLoader
加载一个用户自定义的com.example.MyClass
。
AppClassLoader
首先检查自己是否已经加载过com.example.MyClass
,如果没有,则调用parent.loadClass()
,即委派给ExtClassLoader
。ExtClassLoader
同样,先检查自己是否加载过,如果没有,则调用它的parent.loadClass()
。但ExtClassLoader
的父加载器是Bootstrap ClassLoader
(在代码中体现为parent
是null
)。Bootstrap ClassLoader
(在代码中是findBootstrapClassOrNull
逻辑)在它的核心库路径下查找com.example.MyClass
,显然找不到,返回null
。ExtClassLoader
发现父加载器Bootstrap
没找到,于是开始用自己的findClass()
方法在扩展库路径(ext
目录)下查找,也找不到。AppClassLoader
收到ExtClassLoader
也加载失败的通知(返回null
),于是开始用自己的findClass()
方法在用户类路径(ClassPath)下查找。- 最终,
AppClassLoader
在classpath
下找到了com.example.MyClass
的.class
文件,将其加载到内存并生成Class对象。
五、如何打破双亲委派?
双亲委派模型不是一个强制性的约束,而是 Java 设计者推荐给开发者的一种类加载器实现方式。在复杂的环境中,有时需要打破它。
打破的方式就是重写loadClass()
方法。因为双亲委派的逻辑就封装在这个方法里。如果你重写了它,并且不调用parent.loadClass()
,那就意味着打破了委派链。
历史上打破双亲委派的例子:
-
JDBC SPI (Service Provider Interface)
java.sql.Driver
接口在rt.jar
中,由Bootstrap ClassLoader
加载。- 各个数据库厂商的实现(如
mysql-connector-java.jar
中的驱动)在ClassPath下,理应由AppClassLoader
加载。 - 但根据双亲委派,
Bootstrap ClassLoader
无法“看到”AppClassLoader
加载的类,这就产生了问题。 - 解决方案是引入了线程上下文类加载器(Thread Context ClassLoader),它可以将加载SPI实现的类加载器(通常是
AppClassLoader
)“设置”到线程中,这样核心库的代码(由Bootstrap
加载)就可以通过这个上下文类加载器去加载第三方实现。这是一种“父级委托子级”的反常操作,打破了双亲委派。
-
OSGi、Tomcat 等框架
- 这些容器/module系统有更复杂的类隔离和热部署需求,它们都重写了
loadClass()
逻辑,实现了自己独特的加载规则(如优先加载自己模块下的类,找不到再委派),完全打破了双亲委派。
- 这些容器/module系统有更复杂的类隔离和热部署需求,它们都重写了
总结
特性 | 说明 |
---|---|
核心思想 | 自底向上委派,自顶向下尝试加载。 |
目的 | 保证类的唯一性、安全性和稳定性。防止核心API被篡改。 |
关键方法 | ClassLoader.loadClass() (委派逻辑在此),findClass() (自定义加载逻辑应重写此方法)。 |
如何打破 | 重写loadClass() 方法,不调用父类的实现。 |
常见场景 | JDBC SPI使用线程上下文类加载器打破;OSGi、Tomcat为实现模块化/热部署而打破。 |