一、💡 一句话理解
核心结论
类加载器收到加载请求后,通常先让父加载器尝试;父加载器找不到,自己才加载。这样可以优先复用上层已经加载的类,并保护 Java 核心类的加载秩序。
二、🧭 理论:它是什么
2.1 先找“上级”,再自己处理
双亲委派是 ClassLoader 使用的一种类加载委派机制。
这里的“父”不是 Java 继承关系,而是类加载器内部保存的父加载器。每个类加载器通常只有一个父加载器,“双亲”只是中文习惯叫法。
可结合 12.Java类加载器有哪些 一起理解。
三、⚙️ 理论:它是怎么工作的
3.1 核心流程
关键点是:先一路向上询问,找不到时,再逐层向下自己尝试。
第一阶段是向上委派:
应用类加载器收到请求
↓ 询问父加载器
平台类加载器
↓ 询问父加载器
启动类加载器启动类加载器先查找。它能加载就直接返回结果;找不到时,才进入第二阶段——向下回退:
启动类加载器:没找到
↑
平台类加载器:那我自己尝试
↑
应用类加载器:平台也没找到,那我自己尝试例如加载 java.lang.String:
应用 → 平台 → 启动
↓
启动加载器找到
↓
返回结果例如加载自己编写的 UserService:
应用 → 平台 → 启动
↓ 没找到
平台加载器自己找
↓ 也没找到
应用加载器自己找
↓
在项目 classpath 中找到因此,ClassLoader.loadClass() 的关键顺序可以记成:
- 检查这个类是否已经加载。
- 委派给父加载器加载。
- 父加载器无法加载时,调用自己的
findClass()。
记忆口诀
先问上级,上级不会,自己再会。
主要作用
避免同一个类被层层重复加载,并让
java.lang.String等核心类优先由启动类加载器处理,而不是随意使用应用中的同名类。
四、🚀 实践:查看委派关系
4.1 前置准备
准备一个可用的 JDK,无需额外依赖。
4.2 可以拿来干什么
查看类加载器不是日常业务功能,主要用于定位依赖冲突,以及理解插件、容器和热部署的实现方式。
4.2.1 排查依赖版本冲突
假设项目需要 tool-2.0.jar,但运行环境中还残留了 tool-1.0.jar。编译时使用 2.0 版本,运行时却先加载了 1.0 版本,就可能出现:
NoSuchMethodError
ClassNotFoundException
NoClassDefFoundError这时只看项目的依赖配置不一定够,因为真正重要的是:运行时最终加载了哪个类,以及这个类来自哪个 JAR。
System.out.println(Tool.class.getClassLoader());
System.out.println(
Tool.class.getProtectionDomain()
.getCodeSource()
.getLocation()
);如果输出的是 tool-1.0.jar,就说明运行时加载的依赖不是预期版本。接下来应检查重复依赖、启动参数中的 classpath,以及容器自身提供的公共依赖。
4.2.2 排查“同一个类却不能转换”
JVM 判断一个类的身份时,不只看类名,而是同时看:
类的完整名称 + 定义它的类加载器假设插件 A 和插件 B 都包含 com.example.User:
插件 A 的类加载器 → com.example.User
插件 B 的类加载器 → com.example.User虽然两个类的名字完全一样,但定义它们的类加载器不同,因此 JVM 会把它们当成两个不同的类型。把 A 创建的对象转换成 B 的 User 时,可能出现看似奇怪的错误:
com.example.User cannot be cast to com.example.User排查时,应同时打印对象实际类型的类加载器,以及目标类型的类加载器,确认它们是否一致。
4.2.3 插件和应用隔离
IDE、Tomcat 等程序可能同时运行多个插件或应用。它们可以为不同插件或应用创建独立的类加载器:
插件 A 的类加载器 → Jackson 2.13
插件 B 的类加载器 → Jackson 2.17这样两个插件可以使用不同版本的依赖,不必全部强制升级到同一个版本。隔离的代价是:跨类加载器传递对象时,必须保证双方共享的接口或数据类型由共同的父加载器加载,否则容易出现类型不兼容。
4.2.4 热部署
JVM 已经加载的类通常不能直接用新字节码覆盖。常见做法是:
停止旧模块
↓
释放旧类加载器及其加载的类
↓
创建新类加载器
↓
加载更新后的 class 或 JARTomcat、开发工具和插件系统可以利用这种方式更新部分代码,而不必关闭整个 JVM。需要注意:如果线程、静态变量或缓存仍然引用旧类加载器,旧类和相关对象就可能无法被垃圾回收,造成类加载器泄漏。
4.2.5 为什么项目类和核心类的结果不同
项目代码和 classpath 中的第三方依赖通常由应用类加载器负责,因此打印项目类的 getClassLoader() 时,可以看到一个应用类加载器对象。
String 等 JDK 核心类由启动类加载器负责。启动类加载器属于 JVM 内部实现,没有表示成普通 Java 对象,所以:
System.out.println(String.class.getClassLoader());通常输出:
null这里的 null 表示“由启动类加载器加载”,并不是 String 没有经过类加载。
4.3 完整实践:代码、运行和验证
下面同时查看一个类由谁加载,以及它实际来自哪个目录或 JAR。新建 ParentDelegationDemo.java:
import java.security.CodeSource;
public class ParentDelegationDemo {
public static void main(String[] args) {
// 查看项目类:通常由应用类加载器从 classpath 中加载
printClassInfo(ParentDelegationDemo.class);
// 查看核心类:通常由启动类加载器加载
printClassInfo(String.class);
}
private static void printClassInfo(Class<?> clazz) {
// getClassLoader:查看定义这个类的类加载器
ClassLoader loader = clazz.getClassLoader();
// getCodeSource:查看字节码来自哪个目录或 JAR
// JDK 核心类通常没有普通的 CodeSource,因此需要判断 null
CodeSource source = clazz.getProtectionDomain().getCodeSource();
System.out.println("类名:" + clazz.getName());
System.out.println("类加载器:" + loader);
System.out.println("来源位置:" +
(source == null ? "JDK 运行时镜像或未知来源" : source.getLocation()));
System.out.println("---");
}
}运行:
javac ParentDelegationDemo.java
java ParentDelegationDemo预期结果:
ParentDelegationDemo显示应用类加载器,并输出当前.class所在目录。String的类加载器显示为null,来源显示为 JDK 运行时镜像或未知来源。
排查依赖冲突时,把示例中的类换成发生问题的第三方类。如果输出的 JAR 路径不是预期版本,就说明运行时加载了错误的依赖。具体类加载器名称可能因 JDK 实现而不同。
还可以直接在真实项目中加入下面这段临时代码:
Class<?> targetClass = 第三方类.class;
System.out.println("类加载器:" + targetClass.getClassLoader());
System.out.println("实际来源:" +
targetClass.getProtectionDomain()
.getCodeSource()
.getLocation());把 第三方类.class 替换成发生异常的类,例如 JSON、日志或数据库驱动中的类。排查完成后删除这段临时代码即可。
五、🎤 面试时怎么流利地说
5.1 回答路线
定义 → 加载顺序 → 解决的问题 → 使用边界。
5.2 30 秒标准回答
双亲委派是类加载器的一种委派机制。一个类加载器收到加载请求后,会先检查是否已经加载,再交给父加载器处理;只有父加载器无法加载时,自己才调用 findClass()。它可以避免类被重复加载,并保证 Java 核心类优先由上层加载器加载。实际工作中,可以通过类加载器和字节码来源定位依赖冲突;插件、容器和热部署也会利用不同类加载器实现隔离。不过双亲委派不是不可打破的硬性规则,特殊场景可能采用不同的加载顺序。
5.3 高频追问
为什么叫双亲?
只是中文译名。一个类加载器通常只有一个父加载器,而且父子关系是组合关系,不是类继承关系。
双亲委派一定不能打破吗?
不是。自定义类加载器可以改写加载逻辑,但普通场景更推荐重写 findClass(),保留 loadClass() 的默认委派流程。
如何确认一个类来自哪个 JAR?
先通过 类名.class.getClassLoader() 查看定义它的类加载器,再通过 getProtectionDomain().getCodeSource().getLocation() 查看实际目录或 JAR。JDK 核心类的 CodeSource 可能为 null,使用前需要判空。
六、📌 总结
- 顺序:先检查、再委派、最后自己加载。
- 作用:减少重复加载,维护核心类的加载秩序。
- 边界:它是常见默认机制,不是 JVM 强制所有类加载器必须遵守的唯一模式。