双亲委派机制(Java 类加载核心机制)
双亲委派机制是 Java 类加载器(ClassLoader) 加载类时遵循的核心规则,核心思想是:一个类加载器要加载某个类时,不会先自己尝试加载,而是先委派给它的 "父类加载器" 去加载;只有当父类加载器无法加载(找不到该类)时,才会由当前类加载器自己尝试加载。
这里的 "双亲" 并非指 Java 中的继承关系(不是 extends),而是一种「委派层级关系」,目的是保证类加载的 唯一性 和 安全性。
一、先搞懂:Java 的类加载器层级(委派的 "父子" 关系)
Java 中默认有 3 层核心类加载器(自上而下,父→子),还有自定义类加载器(最底层),层级如下:
| 类加载器 | 作用(加载范围) | 特点 |
|---|---|---|
| 启动类加载器(Bootstrap ClassLoader) | 加载 JDK 核心类库(如 rt.jar 中的 java.lang.String、java.util.ArrayList 等) |
由 C/C++ 实现,无 Java 对象(getClassLoader() 返回 null) |
| 扩展类加载器(Extension ClassLoader) | 加载 JDK 扩展类库(如 jre/lib/ext 目录下的类) |
Java 实现,父类是启动类加载器(逻辑上) |
| 应用程序类加载器(Application ClassLoader) | 加载用户编写的类(classpath 下的类,比如项目 src 目录编译后的类) | Java 实现,默认的系统类加载器,父类是扩展类加载器 |
| 自定义类加载器(Custom ClassLoader) | 开发者自定义加载逻辑(如加载加密类、网络上的类) | 继承 ClassLoader,父类是应用程序类加载器 |
注意:层级是 "逻辑委派关系",不是 Java 中的继承关系(比如应用程序类加载器的父类是扩展类加载器,通过
getParent()方法获取,而非extends)。
二、双亲委派的核心流程("先找爹,爹不行自己上")
当某个类加载器(比如应用程序类加载器)要加载一个类(如 com.example.User)时,流程如下:
- 委派父类加载:当前类加载器不直接加载,先把 "加载请求" 委派给它的父类加载器(应用程序类加载器 → 扩展类加载器);
- 父类继续委派 :父类加载器也不直接加载,继续向上委派,直到最顶层的 启动类加载器;
- 顶层尝试加载 :启动类加载器检查自己的加载范围(JDK 核心类库),如果能找到这个类,就直接加载并返回该类的
Class对象;如果找不到,就 "向下回退" 加载请求; - 逐层回退尝试:扩展类加载器接收回退的请求,检查自己的加载范围(扩展类库),能加载就返回,不能就继续回退;
- 自身最终加载 :如果所有父类加载器都无法加载,最后由最初发起请求的类加载器(应用程序类加载器)自己尝试加载;如果还是找不到,就抛出
ClassNotFoundException异常。
用通俗的话讲:"儿子要找东西,先让爸爸找,爸爸让爷爷找,爷爷找不到再让爸爸找,爸爸还找不到,儿子自己找,再找不到就说没这东西"。
三、为什么需要双亲委派机制?(核心作用)
1. 保证类的唯一性(避免重复加载)
如果没有双亲委派,多个类加载器可能会加载同一个类(比如用户自己写了一个 java.lang.String),导致 JVM 中出现多个相同全限定名的 Class 对象,破坏类的唯一性(JVM 判断两个类是否相同,需要 "类全限定名 + 加载它的类加载器" 都相同)。
比如:启动类加载器已经加载了 JDK 核心的 java.lang.String,如果应用程序类加载器再加载用户自定义的 java.lang.String,就会出现两个 String 类,导致程序逻辑混乱。而双亲委派机制会让 "先找父类加载",启动类加载器已经加载过,就不会重复加载。
2. 保证 Java 核心类的安全性(防止恶意篡改)
双亲委派机制能保护 JDK 核心类库不被恶意类冒充。比如:
- 开发者无法自定义一个
java.lang.String来替换 JDK 原生的String:因为当加载这个自定义String时,会先委派给启动类加载器,启动类加载器已经加载了 JDK 的String,就不会再加载自定义的String; - 更极端的,开发者无法自定义
java.lang.System等核心类,避免恶意代码篡改核心类的逻辑(比如修改System.exit()的行为)。
四、例外情况:打破双亲委派机制
双亲委派是默认规则,但不是强制的,开发者可以通过自定义类加载器打破它(重写 ClassLoader 的 loadClass() 方法,不遵循委派逻辑)。常见场景:
- Tomcat 等 Web 服务器 :Tomcat 需要为每个 Web 应用隔离类加载(比如不同应用的
com.example.User要独立加载),所以 Tomcat 的类加载器会先自己尝试加载 Web 应用的类,再委派给父类加载器(和双亲委派顺序相反,称为 "反向委派"); - OSGi 框架:OSGi 支持模块化热部署,每个模块有自己的类加载器,加载逻辑灵活,不严格遵循双亲委派;
- JDK 9 之后的模块系统(JPMS):模块系统对类加载有新的规则,部分场景会打破传统的双亲委派。
要彻底搞懂双亲委派机制,我们必须把「理论原理」「JDK 源码实现」「自定义代码验证」三者结合起来 ------ 从源码看规则本质,从自定义代码验证规则作用,再从打破规则的场景理解其灵活性。
Java 中所有类加载器都间接继承 java.lang.ClassLoader,这个抽象类定义了类加载的核心流程,关键是 3 个方法,分工明确:
| 方法名 | 作用 | 是否需要重写 |
|---|---|---|
loadClass(String name) |
类加载的「入口方法」,实现双亲委派的核心逻辑(先委派父类,再自己加载) | 不建议重写(打破双亲委派时才重写) |
findClass(String name) |
「查找类文件」的逻辑(比如从文件、网络、加密文件中读取字节码) | 自定义类加载器必须重写 |
defineClass(String name, byte[] b, int off, int len) |
把字节码数组转换成 JVM 能识别的 Class 对象(禁止重写,JVM 底层实现) |
绝对不能重写 |
核心流程关系 :loadClass(委派逻辑)→ 父类加载失败 → 调用 findClass(找字节码)→ 调用 defineClass(转成 Class 对象)
这三个方法的分工,是理解双亲委派和自定义类加载器的关键!
五、源码解析:双亲委派的核心实现(JDK 8 为例)
双亲委派不是 "约定俗成",而是 ClassLoader 类的 loadClass 方法硬编码的逻辑。我们直接看 JDK 源码(关键部分加注释):
java
运行
java
public abstract class ClassLoader {
// 父类加载器(逻辑委派关系,不是继承关系)
private final ClassLoader parent;
// 类加载的入口方法:加载指定全限定名的类
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false); // 第二个参数 resolve:是否解析类(默认不解析)
}
// 核心实现方法
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 {
if (parent != null) { // 有父类加载器:委派给父类加载
c = parent.loadClass(name, false);
} else { // 没有父类加载器(启动类加载器):调用底层 native 方法查找
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 父类加载器抛出异常:说明父类找不到该类
}
// 步骤 2:父类加载器加载失败(c 还是 null),当前类加载器自己找
if (c == null) {
long t1 = System.nanoTime();
// 调用 findClass:自定义类加载器必须重写这个方法(默认抛出异常)
c = findClass(name);
// 记录统计信息(忽略)
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
}
}
// 步骤 3:如果需要解析类(resolve=true),则解析
if (resolve) {
resolveClass(c);
}
return c;
}
}
// 查找类文件:默认实现直接抛出异常,需要自定义类加载器重写
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
// 把字节码转成 Class 对象:native 方法,禁止重写
protected final Class<?> defineClass(String name, byte[] b, int off, int len) throws ClassFormatError {
return defineClass(name, b, off, len, null);
}
}
源码核心逻辑提炼(对应双亲委派理论):
- 缓存优先:先检查当前类加载器是否已经加载过该类(避免重复加载);
- 向上委派 :没加载过则委派给父类加载器(
parent.loadClass),直到启动类加载器; - 向下回退 :父类加载器找不到(抛
ClassNotFoundException),当前类加载器调用findClass自己找; - 线程安全 :用
synchronized锁保证同一类不会被并发加载。
关键细节:
- 启动类加载器(Bootstrap)是 C/C++ 实现的,没有对应的 Java 对象,所以
parent为null时,会调用findBootstrapClassOrNull(native 方法)让启动类加载器尝试加载; - 自定义类加载器的核心是重写
findClass(实现自己的查找逻辑),而不是loadClass(否则会打破双亲委派)。
六、代码实现 1:验证默认双亲委派机制(看现象)
我们通过两个小实验,验证双亲委派的「唯一性」和「安全性」。
实验 1:查看类的加载器层级
写一个普通类,打印加载它的类加载器及其父类加载器:
java
运行
java
// 普通类:com.example.Demo
package com.example;
public class Demo {
public static void main(String[] args) {
// 1. 获取加载 Demo 类的类加载器
ClassLoader classLoader = Demo.class.getClassLoader();
System.out.println("Demo 的类加载器:" + classLoader); // 应用程序类加载器
// 2. 查看父类加载器(扩展类加载器)
ClassLoader parentLoader = classLoader.getParent();
System.out.println("父类加载器:" + parentLoader);
// 3. 查看扩展类加载器的父类(启动类加载器,返回 null)
ClassLoader grandParentLoader = parentLoader.getParent();
System.out.println("祖父类加载器:" + grandParentLoader);
// 4. 查看 JDK 核心类的加载器(启动类加载器)
ClassLoader stringLoader = String.class.getClassLoader();
System.out.println("String 的类加载器:" + stringLoader);
}
}
运行结果:
plaintext
Demo 的类加载器:sun.misc.Launcher$AppClassLoader@18b4aac2(应用程序类加载器)
父类加载器:sun.misc.Launcher$ExtClassLoader@6d03e736(扩展类加载器)
祖父类加载器:null(启动类加载器,无 Java 对象)
String 的类加载器:null(启动类加载器加载)
结果分析:
- 符合我们之前讲的层级关系:应用程序 → 扩展 → 启动(父类委派);
- 核心类(
String)由启动类加载器加载,验证了「核心类优先加载」的规则。
实验 2:验证「核心类不能被篡改」(双亲委派的安全性)
尝试自定义一个 java.lang.String 类,看看能否替换 JDK 原生的 String:
java
运行
java
// 自定义类:java.lang.String(故意和核心类全限定名相同)
package java.lang;
public class String {
public String() {
System.out.println("恶意 String 类被加载!");
}
public static void main(String[] args) {
new String();
}
}
运行结果:
plaintext
Exception in thread "main" java.lang.SecurityException: Prohibited package name: java.lang
at java.lang.ClassLoader.preDefineClass(ClassLoader.java:662)
at java.lang.ClassLoader.defineClass(ClassLoader.java:761)
...
结果分析:
- 无法运行!因为
java.lang是 JDK 核心包,双亲委派机制会让启动类加载器先尝试加载,但 JVM 禁止自定义核心包下的类(preDefineClass方法校验); - 即使去掉包名限制(假设能编译),启动类加载器已经加载了原生
String,也不会再加载自定义的String,保证了核心类的安全性。
七、代码实现 2:自定义类加载器(遵循双亲委派)
自定义类加载器的核心是「重写 findClass 方法」(实现自己的类查找逻辑),而不重写 loadClass(保留双亲委派逻辑)。
场景:加载指定路径(比如 D:/customClass/)下的 class 文件(不是 classpath 下的类)。
步骤 1:编写一个待加载的普通类
java
运行
// 类:com.example.User(编译后放到 D:/customClass/ 目录下)
package com.example;
public class User {
public void sayHello() {
System.out.println("User: Hello, 双亲委派!");
}
}
步骤 2:编译 User 类
用 javac 编译 User.java,得到 User.class,并按包结构放到指定路径:
plaintext
D:/customClass/com/example/User.class
步骤 3:自定义类加载器(遵循双亲委派)
java
运行
java
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
// 自定义类加载器:加载 D:/customClass/ 下的类
public class CustomClassLoader extends ClassLoader {
// 自定义类的根路径
private static final String CLASS_PATH = "D:/customClass/";
@Override
protected Class<?> findClass(String fullyQualifiedName) throws ClassNotFoundException {
try {
// 1. 把全限定名转成文件路径(com.example.User → com/example/User.class)
String filePath = CLASS_PATH + fullyQualifiedName.replace(".", "/") + ".class";
// 2. 读取 class 文件的字节码
byte[] classBytes = loadClassBytes(filePath);
// 3. 调用父类的 defineClass 方法,把字节码转成 Class 对象(禁止自己重写)
return defineClass(fullyQualifiedName, classBytes, 0, classBytes.length);
} catch (IOException e) {
throw new ClassNotFoundException("类找不到:" + fullyQualifiedName, e);
}
}
// 读取 class 文件的字节码
private byte[] loadClassBytes(String filePath) throws IOException {
FileInputStream fis = new FileInputStream(filePath);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len;
while ((len = fis.read(buffer)) != -1) {
bos.write(buffer, 0, len);
}
fis.close();
return bos.toByteArray();
}
}
步骤 4:测试自定义类加载器
java
运行
java
public class TestCustomClassLoader {
public static void main(String[] args) throws Exception {
// 1. 创建自定义类加载器实例
CustomClassLoader customLoader = new CustomClassLoader();
// 2. 加载 D:/customClass/ 下的 com.example.User 类
Class<?> userClass = customLoader.loadClass("com.example.User");
// 3. 验证类加载器(应该是我们的 CustomClassLoader)
System.out.println("User 类的加载器:" + userClass.getClassLoader());
// 4. 反射调用 sayHello 方法
Object user = userClass.newInstance();
userClass.getMethod("sayHello").invoke(user);
// 5. 验证双亲委派:如果 classpath 下也有 com.example.User,会优先加载哪个?
Class<?> systemUserClass = Class.forName("com.example.User");
System.out.println("系统类加载器加载的 User:" + systemUserClass.getClassLoader());
}
}
运行结果:
plaintext
User 类的加载器:CustomClassLoader@6d03e736
User: Hello, 双亲委派!
系统类加载器加载的 User:sun.misc.Launcher$AppClassLoader@18b4aac2
关键分析:
- 遵循双亲委派 :
customLoader.loadClass会先委派给父类(应用程序类加载器),父类在 classpath 下找不到com.example.User(因为我们把它放到了D:/customClass/),才会调用CustomClassLoader的findClass方法自己加载; - 类的唯一性 :如果 classpath 下也有
com.example.User,应用程序类加载器会先加载,自定义类加载器不会再加载(双亲委派的作用); - 自定义逻辑 :
findClass方法实现了 "从指定路径读取 class 文件",这是自定义类加载器的核心价值(比如加载加密的 class 文件、网络上的 class 文件)。
八、代码实现 3:打破双亲委派机制
双亲委派是默认规则,但可以通过「重写 loadClass 方法」打破它。核心思路:修改 loadClass 的委派顺序,让当前类加载器先自己尝试加载,再委派给父类。
场景:模拟 Tomcat 的类加载逻辑(先加载 Web 应用内的类,再委派父类)
步骤 1:修改自定义类加载器(重写 loadClass)
java
运行
java
public class BreakDelegateClassLoader extends ClassLoader {
private static final String CLASS_PATH = "D:/customClass/";
// 只对 com.example 包下的类打破委派,其他类仍遵循(避免核心类加载异常)
private static final String PACKAGE_PREFIX = "com.example.";
@Override
protected Class<?> loadClass(String fullyQualifiedName, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(fullyQualifiedName)) {
// 1. 检查是否已加载
Class<?> c = findLoadedClass(fullyQualifiedName);
if (c == null) {
try {
// 2. 打破委派:对 com.example 包下的类,先自己加载
if (fullyQualifiedName.startsWith(PACKAGE_PREFIX)) {
c = findClass(fullyQualifiedName); // 自己找
} else {
// 3. 其他类(如 java.lang.String)仍遵循双亲委派,委派给父类
c = super.loadClass(fullyQualifiedName, false);
}
} catch (ClassNotFoundException e) {
// 4. 自己加载失败,再委派给父类(兜底)
c = super.loadClass(fullyQualifiedName, false);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
// 复用之前的 findClass 和 loadClassBytes 方法
@Override
protected Class<?> findClass(String fullyQualifiedName) throws ClassNotFoundException {
try {
String filePath = CLASS_PATH + fullyQualifiedName.replace(".", "/") + ".class";
byte[] classBytes = loadClassBytes(filePath);
return defineClass(fullyQualifiedName, classBytes, 0, classBytes.length);
} catch (IOException e) {
throw new ClassNotFoundException(fullyQualifiedName, e);
}
}
private byte[] loadClassBytes(String filePath) throws IOException {
// 同之前的实现
FileInputStream fis = new FileInputStream(filePath);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len;
while ((len = fis.read(buffer)) != -1) {
bos.write(buffer, 0, len);
}
fis.close();
return bos.toByteArray();
}
}
步骤 2:测试打破双亲委派的效果
java
运行
java
public class TestBreakDelegate {
public static void main(String[] args) throws Exception {
// 1. 创建打破委派的类加载器
BreakDelegateClassLoader breakLoader = new BreakDelegateClassLoader();
// 2. 加载 com.example.User(自己先加载)
Class<?> userClass = breakLoader.loadClass("com.example.User");
System.out.println("打破委派后,User 的加载器:" + userClass.getClassLoader());
// 3. 加载核心类 java.lang.String(仍遵循委派,启动类加载器加载)
Class<?> stringClass = breakLoader.loadClass("java.lang.String");
System.out.println("String 的加载器:" + stringClass.getClassLoader());
}
}
运行结果:
plaintext
打破委派后,User 的加载器:BreakDelegateClassLoader@6d03e736
String 的加载器:null
关键分析:
- 打破逻辑 :对
com.example包下的类,先调用findClass自己加载,父类加载器不再优先; - 核心类保护 :对非自定义包的类(如
java.lang.String),仍遵循双亲委派,避免破坏 JVM 核心逻辑; - 实际意义 :Tomcat 就是这样实现的 ------ 每个 Web 应用有自己的类加载器,先加载应用内的类(
WEB-INF/classes),再委派给父类加载器,实现不同应用的类隔离(比如两个应用的com.example.User互不干扰)。
九、关键问题答疑(彻底扫清盲区)
1. 为什么 JVM 判断两个类是否相同,需要 "类全限定名 + 类加载器"?
比如:用 CustomClassLoader 和系统类加载器分别加载 com.example.User,得到的两个 Class 对象是不同的:
java
运行
CustomClassLoader customLoader = new CustomClassLoader();
Class<?> user1 = customLoader.loadClass("com.example.User");
Class<?> user2 = Class.forName("com.example.User"); // 系统类加载器加载
System.out.println(user1 == user2); // false
- 原因:双亲委派机制保证了 "同一个类只被一个类加载器加载",但如果打破委派,多个类加载器可能加载同一个类;
- 影响:如果两个
User类的实例互相赋值,会抛出ClassCastException(类型转换异常)。
2. 启动类加载器为什么 getParent() 返回 null?
- 启动类加载器(Bootstrap)是 JVM 的一部分,由 C/C++ 实现,没有对应的 Java 对象实例;
ClassLoader类中,parent为null时,会调用findBootstrapClassOrNull这个 native 方法,让 JVM 底层的启动类加载器尝试加载。
3. JDK 9 之后的模块系统(JPMS)对双亲委派有影响吗?
- 有!JDK 9 引入模块系统后,类加载逻辑更复杂:模块会明确声明依赖,类加载器会先从模块路径(module path)加载,再从类路径(classpath)加载;
- 但核心思想不变:仍优先加载核心模块的类,避免重复加载和恶意篡改,只是层级和查找顺序有调整。
4. 自定义类加载器必须重写 findClass 吗?
- 是的!因为
ClassLoader的默认findClass方法直接抛出ClassNotFoundException; - 重写
findClass是 "遵循双亲委派" 的正确方式,重写loadClass是 "打破委派" 的方式,不要混淆。
总结:双亲委派的核心逻辑闭环
- 理论本质:向上委派父类加载,向下回退自身加载,保证类的唯一性和核心类安全;
- 源码实现 :
ClassLoader.loadClass方法硬编码了委派逻辑,findClass负责实际查找,defineClass负责字节码转 Class; - 代码验证 :
- 遵循委派:重写
findClass,实现自定义查找逻辑; - 打破委派:重写
loadClass,修改委派顺序;
- 遵循委派:重写
- 实际价值:默认规则保护 JVM 核心,打破规则支持灵活场景(Tomcat 隔离、OSGi 热部署)。
理解双亲委派的关键,不是死记流程,而是能通过源码看懂 "为什么这么设计",通过自定义代码验证 "设计的作用",最终能解释 "实际开发中的相关问题"(比如类冲突、类转换异常)。