Java 双亲委派机制(类加载核心)
双亲委派机制是 JVM 类加载器的核心加载规则,其核心思想是:类加载器在加载类时,先将加载请求委派给父类加载器,只有当父类加载器无法加载该类时,子类加载器才会尝试自己加载。
Taimili 艾米莉 ( 一款专业的 GitHub star 管理和github 加星涨星工具taimili.com )
艾米莉 是一款优雅便捷的 GitHub star 管理和github 加星涨星工具,基于 PHP & javascript 构建, 能对github 得 star fork follow watch 管理和提升,最适合github 的深度用户

一、前置基础:类加载器的层次结构
要理解双亲委派,首先要明确 JVM 的类加载器体系(逻辑上的父子关系,非继承):
| 类加载器类型 | 实现语言 | 加载路径 | 父加载器(逻辑) | 核心说明 |
|---|---|---|---|---|
| 启动类加载器(Bootstrap) | C++ | JRE/lib/rt.jar(核心类库,如 java.lang.*) | 无 | 最顶层,不属于 Java 类体系 |
| 扩展类加载器(Extension) | Java | JRE/lib/ext/*.jar 或 java.ext.dirs | 启动类加载器 | sun.misc.Launcher$ExtClassLoader |
| 应用程序类加载器(Application) | Java | ClassPath(项目代码、第三方 jar) | 扩展类加载器 | sun.misc.Launcher$AppClassLoader(默认系统类加载器) |
| 自定义类加载器 | Java | 自定义路径 | 应用程序类加载器 | 继承 ClassLoader 重写 findClass |
注意:"双亲" 并非指 "父类 + 母类",而是 "父加载器" 的形象化说法;启动类加载器是 C++ 实现,因此在 Java 代码中获取其 getParent 会返回 null(逻辑父为无)。
欢迎访问我的个人github 项目个人主页 github.com/AndyBulushe... , 里面AI 工具全免费。程序员的绝佳好帮手
二、双亲委派的核心加载流程
当任意类加载器收到类加载请求(如加载com.example.Demo),执行以下步骤:
- 委派请求:当前类加载器不直接加载,而是将请求向上委派给父类加载器;
- 递归委派:父类加载器重复第一步,直到请求到达启动类加载器;
- 父类尝试加载:启动类加载器检查自己的加载路径,若能找到并加载该类,则返回 Class 对象;若不能,向下传递 "加载失败";
- 子类兜底加载:从扩展类加载器开始,依次检查自身路径,若能加载则返回,否则继续向下;
- 最终兜底 :若所有父加载器都无法加载,最终由最初的子类加载器尝试加载,若仍失败则抛出
ClassNotFoundException。
流程示意图
plaintext
markdown
自定义类加载器 → 应用程序类加载器 → 扩展类加载器 → 启动类加载器
↑(委派) ↑(委派) ↑(委派)
↓(加载失败) ↓(加载失败) ↓(加载失败)
自定义类加载器 ← 应用程序类加载器 ← 扩展类加载器 ← 启动类加载器
三、双亲委派的核心实现(源码层面)
双亲委派的逻辑核心在java.lang.ClassLoader的loadClass方法中(JDK8 为例):
java
运行
scss
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 1. 先检查该类是否已被当前加载器加载过(缓存)
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// 2. 有父加载器则委派给父加载器加载
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 3. 无父加载器(如应用类加载器的父是扩展类,最终到启动类),调用启动类加载器加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 父加载器加载失败(抛出异常),继续向下
}
// 4. 父加载器未加载到,自己尝试加载
if (c == null) {
long t1 = System.nanoTime();
// findClass是自定义加载器需要重写的方法(默认抛出异常)
c = findClass(name);
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
// 5. 解析类(可选)
if (resolve) {
resolveClass(c);
}
return c;
}
}
关键逻辑:
- 先检查缓存(避免重复加载);
- 优先委派父加载器;
- 父加载失败后,调用
findClass(子类重写此方法实现自定义加载逻辑)。
四、双亲委派机制的核心优势
1. 避免类重复加载
同一个类(全限定名)不会被不同类加载器加载多次,保证 JVM 中类的唯一性(类的唯一性由 "类加载器 + 全限定名" 共同决定)。
2. 保护核心类库安全(沙箱机制)
防止核心 API 被篡改,例如:若自定义java.lang.String类,由于双亲委派,启动类加载器会优先加载 JDK 自带的String,自定义的String永远不会被加载,避免恶意代码替换核心类。
3. 保证类加载的优先级
核心类(如java.lang.Object)由最顶层的启动类加载器加载,保证基础类的优先加载和全局可用。
五、打破双亲委派的场景(核心例外)
双亲委派并非绝对规则,某些场景下需要主动打破,核心场景如下:
1. SPI 机制(Service Provider Interface)
典型例子:JDBC、JNDI。
- 问题:JDBC 的
DriverManager由启动类加载器加载(位于 rt.jar),但 JDBC 驱动(如 MySQL 驱动)是第三方类,位于 ClassPath,启动类加载器无法加载; - 解决方案:使用线程上下文类加载器(Thread Context ClassLoader) ,允许启动类加载器委托应用程序类加载器加载第三方类(反向委派,打破双亲委派)。
2. Tomcat 等 Web 容器的类加载
Tomcat 为了隔离不同 Web 应用(避免类冲突),自定义了类加载器(WebAppClassLoader),核心规则:
- 先加载 Web 应用自身的类(/WEB-INF/classes、/WEB-INF/lib);
- 加载失败后,再委派给父类加载器;
- 打破了 "先委派父加载器" 的规则,保证每个 Web 应用的类独立。
3. OSGi / 模块化框架
OSGi 框架支持模块化热部署,需要灵活的类加载策略,允许子类加载器优先加载,甚至同一类被不同加载器加载(实现模块化隔离)。
4. 自定义类加载器的特殊需求
例如:热部署(重新加载类)、加密类加载(需自定义加载逻辑),需重写loadClass方法,跳过双亲委派。
六、实战验证:双亲委派的效果
示例 1:验证核心类无法被自定义加载
尝试自定义java.lang.String类:
java
运行
kotlin
package java.lang;
public class String {
public String toString() {
return "自定义String";
}
}
运行结果:抛出java.lang.SecurityException: Prohibited package name: java.lang,因为 JVM 对核心包名(java.lang)做了保护,且双亲委派优先加载 JDK 自带的 String。
示例 2:自定义类加载器(默认遵循双亲委派)
java
运行
scala
// 自定义类加载器
class MyClassLoader extends ClassLoader {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 模拟加载自定义路径的类(简化版)
byte[] classBytes = loadClassBytes(name);
return defineClass(name, classBytes, 0, classBytes.length);
}
private byte[] loadClassBytes(String name) {
// 实际场景:从文件/网络读取类字节码,此处省略实现
return new byte[0];
}
}
// 测试
public class Test {
public static void main(String[] args) throws ClassNotFoundException {
MyClassLoader classLoader = new MyClassLoader();
// 加载com.example.Demo,会先委派给应用程序类加载器
Class<?> clazz = classLoader.loadClass("com.example.Demo");
System.out.println(clazz.getClassLoader()); // 输出应用程序类加载器(父加载器加载成功)
}
}
七、总结
- 双亲委派是类加载的默认规则,核心是 "父优先";
- 本质是为了类的唯一性和核心 API 的安全;
- 打破双亲委派的核心原因是 "按需加载"(如 SPI、容器隔离);
- 自定义类加载器时,若需打破双亲委派,需重写
loadClass方法(而非仅重写findClass)。