主题
界面
主题
「双亲委派」四个字谁都会背,但能亲手打破它、并亲眼看到后果的人不多。
★ 本节用自写的类加载器做了 5 组实验,其中两组是「反常识」的:
自定义加载器读同一个 .class 文件,却「不生效」——因为父加载器先命中 ✓✓(委派生效)
强制跳过父加载器后:同名同字节码,instanceof 一个 true 一个 false ✗✗(类型不兼容)
切断引用 + System.gc() → 加载器被回收 true ✓✓(类随之卸载)static 字段与静态块吗?(《类与对象》)✓★ 线上三类「看起来毫无道理」的故障,根都在类加载 ✗✗:
| 现象 | 真正原因 |
|---|---|
ClassNotFoundException / NoClassDefFoundError ✗✗ | 前者「加载时找不到」,后者「加载过但初始化失败」 |
| 改了代码不生效 ✗✗ | 加载器没换,旧类还在用(Tomcat 热部署的坑) |
| 同一个类强转失败 ✗✗✗ | 类名相同、加载器不同 —— 本节将亲手复现 |
加载 → 验证 → 准备 → 解析 → 初始化 → 使用 → 卸载
(准备:静态字段赋零值)(初始化:静态块执行、赋真值)★★ 关键分界:「加载」不等于「初始化」 ✓✓✓ —— 初始化只发生一次,且推迟到首次真正使用时(new、访问静态字段、反射调用等)。
★ 主动引用的六种触发场景(其余如读编译期常量不触发)✓:
| 会触发初始化 ✓ | 不会触发 ✗ |
|---|---|
new、访问静态字段/方法、反射 Class.forName(默认) | 读 static final 编译期常量(被内联) |
| 初始化子类时父类先初始化 | ClassLoader.loadClass(只加载) |
| 作为 main 类的入口 | 类字面量 Xxx.class(只加载) |
String.class → null(Bootstrap,C++ 实现,看不见)
javax.sql.DataSource → jdk.internal.loader.ClassLoaders$PlatformClassLoader@7852e922
ClassLoadingLesson → jdk.internal.loader.ClassLoaders$AppClassLoader@105be200
App 的父加载器 = PlatformClassLoader
Platform 的父加载器 = null(Bootstrap)| 加载器 | 加载什么 | 可见性 |
|---|---|---|
| Bootstrap ★★★ | java.* 核心库(<JAVA_HOME>/lib) | 最顶层,Java 代码里看不到 |
| Platform ★★ | 部分标准模块(如 java.sql 的一部分) | 位于中间层 |
| App ★★★ | classpath(你的代码与三方 jar) | 最底层,最常用 |
★ 双亲委派的流程 ✓✓✓:收到加载请求 → 先交给父加载器 → 父加载器逐层上报到 Bootstrap → 父辈都加载不了,自己才加载。
★★★ 三个作用(实测背书):
java.lang.String 也加载不了(会被委派给 Bootstrap)✓✓;把 MirrorClassLoader 改成对目标类「自己先加载」后:
是同一个 Class 吗? false ✗✗(自己先加载,父辈被跳过)
app 版实例 instanceof ProbeSubject → true ✓
自定义版实例 instanceof ProbeSubject → false ✗✗
强转直接抛 ClassCastException:class cn.kb.learn.deep.jvm.ProbeSubject cannot be cast to
class cn.kb.learn.deep.jvm.ProbeSubject (…is in unnamed module of loader
cn.kb.learn.deep.jvm.MirrorClassLoader @12edcd21; …is in unnamed module of loader 'app')★★★ 类的唯一标识 = 「全限定类名 + 定义它的类加载器」 ✓✓✓ —— 同名同字节码,只要加载器不同,就是两个互不兼容的类型 ✗✗。
★ 现实里谁在打破委派 ✓:
| 场景 | 做法与目的 |
|---|---|
| Tomcat 热部署 ★★ | 每个 Web 应用一个独立加载器:重发时换新加载器 = 换新类 |
| SPI(JDBC 驱动) ★★ | Bootstrap 加载接口、线程上下文加载器加载实现 |
| OSGi / 插件化 ★ | 按模块隔离,同名类可以共存 |
切断全部强引用 + System.gc() 后,加载器被回收? true★★ 条件:类的加载器不可达 → 类(连同静态字段)一起卸载 ✓✓ —— 热部署内存泄漏的经典原因:旧加载器被静态字段 / 未结束的线程 / ThreadLocal 意外引用住 ✗✗(呼应 《虚拟线程》 的 ThreadLocal 话题)。
Class.forName 与 loadClass 的差别 Class<?> a = ClassLoader.getSystemClassLoader().loadClass("cn.kb.learn.deep.jvm.InitProbe");
// 只加载,不初始化 —— 静态块不执行 ✓
Class<?> b = Class.forName("cn.kb.learn.deep.jvm.InitProbe");
// 默认 initialize=true —— 静态块在此执行 ✓✓★ 这就是 JDBC 老代码写 Class.forName("com.mysql.cj.jdbc.Driver") 的原因 ✓✓: 驱动类靠静态块把自身注册进 DriverManager —— 必须触发初始化。
public class MirrorClassLoader extends ClassLoader {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Path file = dir.resolve(name.replace('.', '/') + ".class");
byte[] bytes = Files.readAllBytes(file); // 字节码从哪来,你说了算
return defineClass(name, bytes, 0, bytes.length);
}
}★ 加载器只做两件事:找到字节码 → defineClass 定义成类 ✓✓(网络、加密 jar、内存字节码都能当来源)。
System.out.println(SomeClass.class.getClassLoader());★ 排障第一动作 ✓✓:NoClassDefFoundError / 类型冲突时,先确认两边的加载器是不是同一个。
代码:labs/java-deep/src/main/java/cn/kb/learn/deep/jvm/ClassLoadingLesson.java(配 InitProbe、ProbeSubject、MirrorClassLoader)
实测输出(关键部分):
===== 1. 三类加载器与委派链 =====
String.class → null(Bootstrap,C++ 实现,看不见)
javax.sql.DataSource → jdk.internal.loader.ClassLoaders$PlatformClassLoader@7852e922
ClassLoadingLesson → jdk.internal.loader.ClassLoaders$AppClassLoader@105be200
===== 2. 加载 ≠ 初始化:静态块什么时候执行 =====
第 1 步:读编译期常量 InitProbe.TAG(会被内联,不触发初始化)
tag = probe(上面没有静态块输出 ✓)
第 2 步:ClassLoader.loadClass(只加载、不初始化)
已装入 cn.kb.learn.deep.jvm.InitProbe(仍没有静态块输出 ✓)
第 3 步:Class.forName(默认 initialize=true)
[静态块执行] InitProbe 正在初始化 → 静态字段赋真值
两个 Class 是同一个吗? true(同一个 Class,只是初始化晚了)
第 4 步:再次访问静态字段(不重复打印静态块)
InitProbe.counter = 42
===== 3. 双亲委派:先问父辈 =====
是同一个 Class 吗? true ← 父加载器先命中,只定义了一次 ✓✓
===== 4. 打破委派:同一个类名,两个 Class 对象 =====
是同一个 Class 吗? false ✗✗(自己先加载,父辈被跳过)
加载器分别是:ClassLoaders$AppClassLoader / MirrorClassLoader
app 版实例 instanceof ProbeSubject → true ✓
自定义版实例 instanceof ProbeSubject → false ✗✗
===== 5. 类的卸载:加载器不可达 = 类可卸载 =====
切断全部强引用 + System.gc() 后,加载器被回收? true★ 第 5 组的注意点 ✓:GC 不保证立即回收(本节多轮触发才稳定观察到)。
getClassLoader(),把「谁加载的」问到底 ✓✓✓;java -verbose:class 观察类加载顺序与加载器 ✓✓;MirrorClassLoader 的 selfFirstClass 换成 java.lang.String 试试 ✗✗(defineClass 会被安全机制拒绝)✓;-Xlog:class+load 在 JDK 21 下看加载日志与来源 jar ✓。| # | 错误 | 说明 |
|---|---|---|
| 1 | 把 loadClass 当初始化用 ✗✗ | 静态块不会执行(JDBC 驱动就靠 forName)✓✓ |
| 2 | 两个同名类靠强转打通 ✗✗✗ | 不同加载器 = 不同类型,必然 ClassCastException ✓ |
| 3 | NoClassDefFoundError 当「没引入依赖」 ✗ | 它常是「初始化失败」——查静态块的异常 ✓ |
| 4 | 热部署后内存持续上涨 ✗✗ | 旧加载器被静态字段/线程引用住 ✓ |
| 5 | 自定义加载器忘了 defineClass 的包名归属 ✗ | defineClass 的 name 必须与字节码一致 ✓ |
ClassLoadingLesson 五组实验 ✓✓✓;ProbeSubject 的字节码改一个字节再加载,观察 LinkageError ✓✓;-Xlog:class+load 观察自己程序的类从哪个 jar 来 ✓;验收方式:能对一段「改了代码不生效」「同名类强转失败」的现场,说出加载器层面的原因与验证步骤。
扩展任务:写一个「类冲突检测」小工具:列出指定 jar 包被哪些加载器加载过(呼应线上 NoSuchMethodError 排查)✓✓
ClassNotFoundException 与 NoClassDefFoundError 的区别?按顺序学 《垃圾回收算法与收集器》(垃圾回收算法与收集器)→ 《GC 日志分析与 JVM 调优》(GC 日志分析与 JVM 调优)。 本节思路同时支撑 《Arthas 与线上问题诊断》(Arthas 用 sc -d 查类的加载器)与 《字节码与 Java Agent》(Agent 如何改写类文件)。