臭名昭著aTrust隔离计划1.臭名昭著aTrust隔离计划2.VPN原理与aTrust隔离网络实践3.docker-easyconnect到底做了什么4.TUN(tunnel-隧道-虚拟网卡)模式
个人项目
personal(个人展示)GitHub Actions 自动部署个人展示项目
基于Quartz搭建的个人博客1.Quartz个人博客使用教程2.使用 rsync 增量部署 Quartz 博客3.使用 GitHub Actions 自动部署 Quartz 博客5.域名绑定6.CDN加速7.Google 搜索收录排查:为什么关键词搜得到别人,却搜不到我的网站?Obsidian、双向链接与知识图谱
架构设计
分布式分布式事务RPC原理
旅行
行程攻略青岛三日游行程攻略
前端
NginxNginx配置与反向代理入门
Nodenpm与npx的区别
数据库
mysql01.MySQL基础语法与约束02.MySQL数据库类型03.MySQL多表查询04.MySQL 索引05.MySQL 事务06.MySQL 隔离级别07.MySQL MVCC08.MySQL 锁09.EXPLAIN10.慢查询与 SQL 优化
mysql面试题01.索引问题02.事务与隔离级别03.锁机制04.存储引擎与性能优化05.用户权限、字符集与备份恢复
mysql实战01.EXPLAIN实战02.测试环境菜单误删排查与恢复03.MySQL IN 详解
网络
网络安全1.无线监听网卡苹果M系列虚拟机安装Kali Linux教程3.kali怎么使用RTL8812AU4.kali基础命令
网络基础进程之间怎样通过网络传递数据
网址常用网址
中间键
esElasticsearch 入门
mq
RabbitMQRabbitMQ高级RabbitMQ基础
1. MQ 基础2. MQ 核心角色与代码对应3. MQ Consumer Group 与消费模式4. SOFAMQ 账户变更代码实践5. MQ 可靠性问题6. 消息幂等
redis缓存击穿、穿透与雪崩Redis 分布式锁Redis 缓存问题Redis 数据结构
AI
使用说明
第三方插件&技能简介Archify使用与安装指南geekgeekRun使用OfferFlow使用指南Ponytail使用指南Skills Manager使用指南
CodexCodex CLI与IDE区别及使用指南Codex Hook单独配置与提交通知Codex MCP安装与使用指南Codex第三方插件安装与使用指南
Agent开发0-学习路线1.LLM、Token、上下文窗口与模型参数2.大模型 API、请求参数与响应结构3.Spring AI ChatClient4.Prompt、System Prompt、Prompt 模板6.结构化输出、JSON Schema7.SSE 流式响应8.会话 ID、聊天记录、Redis9.超时、重试、限流、降级10.*完成可运行聊天接口11.Function Calling、Tool Calling 原理12.用 Java 方法定义 Tool13.工具参数设计和校验14.实现订单查询工具15.实现物流查询工具16.实现库存查询工具17.多个工具自动选择18.工具异常、超时、重试
diy主机
主机零件学习1.CPU2.主板3.内存4.显卡5.硬盘6.散热7.风扇8.机箱热门型号价格表
捡垃圾diy-13600kf+5060diy-14600kf+5070
English高频词汇英文基础语法简单句子音标
GitGit常用命令Sublime Merge 使用指南
Java
工具库Caffeine本地缓存Reactor教程
工作实践如何使用@Async线程池使用与排查线上服务 CPU 使用率过高排查压测下RPC接口大量失败排查记录
基础1.HashMap2.ConcurrentHashMap3.HashSet4.final5.static6.volatile7.AtomicInteger8.String动态代理反射锁与线程同步线程注解
面试题1.HashMap 存储在方法内是线程安全的吗2.ArrayList参数传递与引用重新赋值3.== 和 equals 的区别、hashcode4.重载和重写的区别5.String、StringBuffer、StringBuilder区别及使用场景6.接口和抽象类7.ArrayList 与 LinkedList 的区别和扩容机制8.HashMap 和 Hashtable 的区别及底层实现9.Object 和 Objects 的区别10.ThreadLocal的原理和使用场景11.字节码是什么及作用12.Java类加载器有哪些13.双亲委派模型14.Java异常体系15.GC是什么16.GC如何判断对象可以被回收
面试总结0.HR面试模板1.面试一2.面试二3.面试三笔试
网络与 IO(黑马程序员-纯理论)01-网络编程基础02-IO模型与零拷贝03-BIO04-NIO基础与缓冲区05-NIO通道与选择器06-AIO
juc00-目录01-进程与线程02-线程同步与通信03-并发设计模式04-Java内存模型05-CAS与原子类06-ThreadLocal07-阻塞队列08-线程池使用与原理09-任务调度10-ForkJoin与享元模式11-AQS12-ReentrantLock13-读写锁14-并发协作工具15-ConcurrentHashMap16-CopyOnWrite与同步容器17-SkipListMap18-非阻塞队列
juc(工作常见)Java并发编程三个重点拆解
jvm1. JVM运行时数据区
Spring1.Spring IoC、DI与构造方法注入CGLIB、Byte Buddy 与 Spring AOPSpring 常用注解详解Spring Bean 生命周期
Spring MVC异常处理机制
linux常用命令
mac快捷键终端常用操作
python
基础01.Python是什么,为什么要安装Python02.Python项目中.venv是什么03.Python基础语法
基础实战sql替换
HTTPXHRMS员工详情接口调用(Python HTTPX)
windowswindows激活

一、💡 一句话理解

核心结论

类加载器收到加载请求后,通常先让父加载器尝试;父加载器找不到,自己才加载。这样可以优先复用上层已经加载的类,并保护 Java 核心类的加载秩序。

二、🧭 理论:它是什么

2.1 先找“上级”,再自己处理

双亲委派是 ClassLoader 使用的一种类加载委派机制。

这里的“父”不是 Java 继承关系,而是类加载器内部保存的父加载器。每个类加载器通常只有一个父加载器,“双亲”只是中文习惯叫法。

可结合 12.Java类加载器有哪些 一起理解。

三、⚙️ 理论:它是怎么工作的

3.1 核心流程

关键点是:先一路向上询问,找不到时,再逐层向下自己尝试。

第一阶段是向上委派:

应用类加载器收到请求
        ↓ 询问父加载器
平台类加载器
        ↓ 询问父加载器
启动类加载器

启动类加载器先查找。它能加载就直接返回结果;找不到时,才进入第二阶段——向下回退:

启动类加载器:没找到
        ↑
平台类加载器:那我自己尝试
        ↑
应用类加载器:平台也没找到,那我自己尝试

例如加载 java.lang.String:

应用 → 平台 → 启动
                 ↓
           启动加载器找到
                 ↓
              返回结果

例如加载自己编写的 UserService:

应用 → 平台 → 启动
                 ↓ 没找到
       平台加载器自己找
                 ↓ 也没找到
       应用加载器自己找
                 ↓
       在项目 classpath 中找到

因此,ClassLoader.loadClass() 的关键顺序可以记成:

  1. 检查这个类是否已经加载。
  2. 委派给父加载器加载。
  3. 父加载器无法加载时,调用自己的 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 或 JAR

Tomcat、开发工具和插件系统可以利用这种方式更新部分代码,而不必关闭整个 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 强制所有类加载器必须遵守的唯一模式。

七、📚 官方资料