主题
概念卡片:类加载机制与双亲委派
一句话机制
JVM 把 .class 的二进制字节流按「加载→验证→准备→解析→初始化」装入内存并生成 Class 对象,加载时按「双亲委派」自底向上请求、自顶向下加载,保证核心类唯一且安全。 类的静态初始化由 <clinit>() 完成,JVM 保证它多线程安全。
类生命周期 7 阶段
加载 → 验证 → 准备 → 解析 → 初始化 → 使用 → 卸载
└────── 连接(Linking) ──────┘| 阶段 | 做什么 |
|---|---|
| 加载 | 通过全限定名找字节流 → 转成方法区运行时结构 → 生成 Class 对象 |
| 验证 | 4 步:文件格式/元数据/字节码/符号引用,保证字节码合法 |
| 准备 | 为类变量(static)分配内存并设零值 |
| 解析 | 常量池符号引用 → 直接引用 |
| 初始化 | 执行 <clinit>(),真正执行 static 块 + 静态变量赋值 |
| 卸载 | Class 对象被 GC(需满足 3 条件) |
不变量(准备 vs 初始化赋值时机):
static int a = 1在准备阶段是 0、初始化阶段才赋 1;但static final int C = 10(编译期常量)在准备阶段就赋好值。区分点:是否需要执行代码/putstatic指令——基本类型常量、String 字面量在准备阶段,引用类型(如Integer.valueOf)、new、Random在初始化阶段。
java
public static int a = 1; // 初始化阶段赋值
public static final int C = 10; // 准备阶段赋值
public static final Integer I = Integer.valueOf(100); // 初始化阶段赋值
public static final String s = new String("x"); // 初始化阶段赋值双亲委派模型
类加载请求先交给父加载器,父加载器加载不了才自己加载。 加载顺序自底向上(AppClassLoader → ExtClassLoader → BootstrapClassLoader),加载动作自顶向下。
| 加载器 | 加载范围 |
|---|---|
| BootstrapClassLoader | 核心库(rt.jar/lib,C++ 实现,无父) |
| ExtClassLoader | 扩展库(lib/ext) |
| AppClassLoader | 应用 classpath |
作用:① 避免类重复加载;② 防止核心 API 被篡改(自定义的
java.lang.String永远轮不到自己被加载)。打破双亲委派靠重写loadClass()(如 Tomcat 的 WebAppClassLoader 隔离各应用、JDBC 的 SPI 反向委派)。
<clinit>() 与 <init>()
<clinit>():类的初始化方法,由 static 块 + 静态变量赋值合并自动生成,只出现一次。父类<clinit>先于子类执行。<init>():对象的初始化方法(实例字段 + 构造方法),每次new都调用。- 无 static 赋值/静态块的类不生成
<clinit>()。
⚠️ 隐蔽死锁:
<clinit>()是线程安全的(多线程初始化同一类只有一个线程执行、其余阻塞)。若 A 类的<clinit>里Class.forName("B")、B 类的<clinit>里Class.forName("A"),两线程各持一锁互相等待 → 死锁,且没有可见的锁信息,极难排查。
主动初始化(6 种触发)
new/getstatic/putstatic/invokestatic4 条字节码指令- 反射调用(
Class.forName()、newInstance()) - 初始化子类先初始化父类
- 启动时初始化 main 方法所在类
MethodHandle/VarHandle的findStaticVarHandle- 接口 default 方法:实现类初始化前,接口先初始化
被动引用(不会初始化):访问父类静态字段、通过数组定义引用、访问编译期常量
static final。
卸载 3 条件
类被卸载(Class 对象被 GC)需同时满足:① 该类的所有实例已 GC;② 类无任何地方被引用;③ 加载它的类加载器实例已 GC。因此 JDK 自带类加载器加载的类永不被卸载,只有自定义类加载器加载的类才可能被卸载(这是热部署/热加载的基础)。
常见误解(避坑)
- ❌ "类加载完就能用"。→ 加载只是第一步,还要经过连接(验证/准备/解析)和初始化才能真正用;
Class.forName()会触发初始化,而ClassLoader.loadClass()只加载不初始化(这是 JDBC 驱动延迟加载的原理)。 - ❌ "static final 常量都在初始化阶段赋值"。→ 编译期常量(基本类型/字面量 String)在准备阶段就赋好;带方法调用/
new的引用类型常量在初始化阶段。 - ❌ "类变量内存一定在方法区"。→ JDK7 起静态变量随
Class对象移到堆中,方法区只留类型信息。 - ❌ "双亲委派不可打破"。→ 重写
loadClass()即可打破(Tomcat/热部署/SPI 都在用)。
关联
- 原始资料:✅【三】类加载机制 · ✅【一】Class类文件结构详解
- 总览:JVM技术栈总览
- 域地图:A00-百科/Java后端/Java后端