Java 双亲委派机制笔记
什么是双亲委派机制
双亲委派机制(Parent Delegation Model)是 Java 类加载器的一种设计模式。在该模式下,类加载器在加载类时会首先把请求交给父加载器加载,父加载器无法完成加载请求时,才由当前加载器尝试自己去加载。
这一机制是 Java 保证类加载安全性、避免重复加载核心类和类一致性的关键机制。
类加载过程简介
在 Java 中,类的生命周期分为以下几个阶段:
- 加载(Loading)
- 验证(Verification)
- 准备(Preparation)
- 解析(Resolution)
- 初始化(Initialization)
- 使用(Using)
- 卸载(Unloading)
双亲委派机制影响的是“加载阶段”,即类从 .class
文件或字节码中被 JVM 加载为 Class
对象的过程。
Java 类加载器的分类
Java 中的类加载器分为以下几种(逻辑上是层级结构,但在 JVM 内部实现上是组合):
1. 启动类加载器(Bootstrap ClassLoader)
- JVM 自身的一部分,用 C/C++ 实现。
- 负责加载 JDK 核心类库,如
rt.jar
或modules/java.base
中的类(如java.lang.*
、java.util.*
等)。 - 加载路径来自
-Xbootclasspath
。
2. 扩展类加载器(Extension ClassLoader)
- 用 Java 实现,父加载器为 Bootstrap。
- 加载
JAVA_HOME/lib/ext/
目录或java.ext.dirs
指定的目录下的类库。
3. 应用类加载器(App ClassLoader / System ClassLoader)
- 加载
classpath
下的类(通常是用户的应用代码)。 - 是大多数 Java 程序默认使用的加载器。
4. 自定义类加载器(User-defined ClassLoader)
- 用户可以通过继承
ClassLoader
或URLClassLoader
实现。 - 可用于隔离、热加载、插件系统等高级功能。
类加载器的父子关系图(逻辑结构)
[Bootstrap ClassLoader]↑[Extension ClassLoader]↑[Application ClassLoader]↑[User-defined ClassLoader]
注意:这种关系是“逻辑上的委派”,实际的实现并非严格的继承,而是通过组合(如构造函数传入父加载器引用)。
双亲委派模型的工作机制
Java 中每个类加载器在加载类时会遵循如下逻辑:
- 检查是否已加载该类(findLoadedClass)
- 委托父加载器尝试加载(parent.loadClass)
- 如果父加载器找不到,则当前类加载器调用
findClass()
自己尝试加载
模拟流程图
loadClass(className):if (hasLoaded(className)) {return loadedClass;} else {try {return parent.loadClass(className);} catch (ClassNotFoundException e) {return findClass(className);}}
这种从上到下的递归式加载,构成了“双亲优先”的委派模型。
双亲委派的优点
1. 避免类重复加载
例如,不同模块都定义了 java.lang.String
,双亲委派确保只加载系统提供的 String
类。
2. 安全性
核心类优先由 Bootstrap 加载,防止恶意代码替换标准 API。
3. 保持类型一致性
类加载器不同,JVM 会认为加载出来的类是不同的,哪怕类名、包名完全一致。双亲委派可以避免多个类加载器加载同一类导致的 ClassCastException
。
双亲委派源码解析(ClassLoader#loadClass()
)
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {// 1. 首先检查是否已经加载Class<?> c = findLoadedClass(name);// 2. 没加载则委托父加载器if (c == null) {try {if (parent != null) {c = parent.loadClass(name, false); // 递归向上委派} else {c = findBootstrapClassOrNull(name); // Bootstrap 加载器处理}} catch (ClassNotFoundException e) {// 忽略,由当前加载器自己加载}}// 3. 父类加载器未加载成功,则由当前类加载器尝试加载if (c == null) {c = findClass(name); // 需要用户在子类中实现}// 4. 如果需要解析类if (resolve) {resolveClass(c);}return c;
}
打破双亲委派机制的情况
尽管双亲委派机制带来了安全性和一致性,但在某些实际场景下,会刻意“打破”这一机制。
常见打破机制的场景
场景 | 描述 |
---|---|
Web 应用容器(如 Tomcat) | 每个应用有独立类加载器,避免类冲突 |
插件系统 / 模块系统(OSGi) | 每个插件或模块有自己类加载器 |
JDBC SPI机制 | 接口由引导加载器加载,实现类由 AppClassLoader 加载 |
热部署 / 热加载 | 需重复加载不同版本类,必须打破委派 |
打破方式
- 重写
loadClass
方法,不调用super.loadClass()
或先自己加载再委派。 - 使用
Thread.currentThread().getContextClassLoader()
代替类自身加载器。 - 使用
URLClassLoader
动态加载 JAR。
示例:SPI机制如何打破双亲委派
SPI接口(如 java.sql.Driver
)是由 Bootstrap 加载的,但其实现类(如 com.mysql.jdbc.Driver
)在用户类路径上,因而无法由 Bootstrap 加载。
解决方法是使用线程上下文类加载器:
Thread.currentThread().setContextClassLoader(MyClassLoader);
自定义类加载器示例
以下是一个基本的自定义类加载器例子:
public class MyClassLoader extends ClassLoader {private final String basePath;public MyClassLoader(String basePath) {this.basePath = basePath;}@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {String path = basePath + name.replace(".", "/") + ".class";try {byte[] classBytes = Files.readAllBytes(Paths.get(path));return defineClass(name, classBytes, 0, classBytes.length);} catch (IOException e) {throw new ClassNotFoundException(name);}}
}
类加载器与类型隔离
在 JVM 中,类是由类的“全限定名 + 加载它的类加载器”决定唯一性的。
if (A.class loaded by Loader1 != A.class loaded by Loader2) {instanceof → falseequals → falsetype cast → ClassCastException
}
这就是插件系统、容器中的类隔离依据。
常见面试问题解析
Q1:什么是双亲委派机制?
是 Java 类加载器在加载类时将加载请求先委托给父类加载器处理,直到 Bootstrap 加载器,只有当父类加载器无法完成加载时,才由当前类加载器处理的一种模型。
Q2:为什么需要双亲委派机制?
为了防止重复加载、类冲突和核心类被篡改,同时也提升安全性和一致性。
Q3:有哪些场景打破了双亲委派机制?
Tomcat 的 WebAppClassLoader、SPI 服务接口机制、插件式架构、Spring Boot DevTools 的热部署等。
Q4:类加载器是线程安全的吗?
是线程安全的,JVM 在加载类时会对类加载过程进行加锁。
Q5:类加载器如何影响类的唯一性?
JVM 通过“类名 + 加载器”唯一标识一个类,不同加载器加载的同名类被视为不同。
总结
- Java 中类的加载采用了 双亲委派模型,即先由父类加载器加载,加载失败后才自己加载。
- 类加载器分为:启动类加载器、扩展类加载器、应用类加载器和自定义类加载器。
- 双亲委派机制带来了安全性、一致性和防重复性,是 Java 平台的重要基石。
- 某些场景如 SPI、Web 容器、热部署系统中,需要打破或改写该模型。
- 理解类加载器和双亲委派机制对于掌握 Java 虚拟机原理、性能优化、框架底层原理(如 Spring、Tomcat、Netty)非常重要。