一、类加载的整体流程(7 阶段)
根据《Java 虚拟机规范》,一个类从 .class 文件到可被 JVM 使用,需经历以下 7 个阶段:
加载(Loading)
↓
验证(Verification)
↓
准备(Preparation)
↓
解析(Resolution)
↓
初始化(Initialization)
↓
使用(Using)
↓
卸载(Unloading)
其中前 5 步属于 类加载过程(Class Loading Process),也是我们重点分析的部分。
⚠️ 注意:"加载" ≠ "初始化"!很多面试者混淆这两个概念。
二、逐阶段详解 + 代码验证
第 1 阶段:加载(Loading)
✅ 做了什么?
- 通过类的全限定名 (如
com.example.Foo)获取其二进制字节流(.class文件内容)。- 来源可以是:
- 本地文件系统(最常见)
- 网络(如 RMI、Applet)
- 数据库(少见)
- 动态生成(如 ASM、CGLIB、Lambda 表达式)
- 来源可以是:
- 将字节流解析为 JVM 内部数据结构(存入方法区 / Metaspace)。
- 在堆中创建一个
java.lang.Class对象,作为程序访问该类的入口。
🔧 谁负责?
- ClassLoader 子类 (如
AppClassLoader、自定义 ClassLoader) - Bootstrap ClassLoader(由 C++ 实现,无法在 Java 中直接引用)
📌 示例:观察"加载"是否发生
java
public class LoadDemo {
static {
System.out.println("LoadDemo 被初始化了!");
}
}
java
public class Main {
public static void main(String[] args) throws Exception {
// 方式1:仅加载,不初始化
Class<?> clazz = ClassLoader.getSystemClassLoader().loadClass("LoadDemo");
System.out.println("类已加载,但未初始化");
// 方式2:触发初始化
Class.forName("LoadDemo"); // 默认 initialize=true
}
}
输出:
类已加载,但未初始化
LoadDemo 被初始化了!
✅ 结论:
loadClass()只执行到"加载"阶段(可能包含验证、准备),不会初始化。Class.forName()默认会执行到"初始化"阶段。
第 2 阶段:验证(Verification)
✅ 做了什么?
确保字节码安全、合法、符合 JVM 规范,防止恶意代码破坏 JVM。分为四步:
| 子阶段 | 作用 |
|---|---|
| 文件格式验证 | 检查魔数(CAFEBABE)、版本号、常量池等是否合法 |
| 元数据验证 | 检查类结构(如继承关系、final 类是否被继承) |
| 字节码验证 | 检查指令是否合法(如操作数栈溢出、类型不匹配) |
| 符号引用验证 | 确保解析阶段能正确找到目标类/方法/字段 |
💡 验证失败会抛出
VerifyError(属于LinkageError)。
📌 为什么需要?
- 安全性:防止伪造的
.class文件破坏 JVM。 - 稳定性:避免运行时崩溃(如非法跳转指令)。
⚠️ 开发中极少遇到,除非手动修改字节码或使用不兼容的编译器。
第 3 阶段:准备(Preparation)
✅ 做了什么?
- 为类变量(static 字段) 分配内存(在方法区)。
- 设置初始值(零值) ,不是赋值语句的值!
📌 关键规则:
| 字段类型 | 初始值 |
|---|---|
int / long |
0 / 0L |
boolean |
false |
| 引用类型 | null |
static final 且为编译期常量 |
直接赋值(因为值已存入常量池) |
🧪 代码验证:
java
public class PreparationDemo {
public static int a = 100; // 准备阶段设为 0,初始化阶段才设为 100
public static final int b = 200; // 编译期常量,准备阶段直接设为 200
public static final String c = "OK"; // 同上
public static String d = "Hello"; // 准备阶段为 null,初始化阶段为 "Hello"
}
java
public class TestPrep {
public static void main(String[] args) throws Exception {
// 仅触发加载+准备,不初始化
Class<?> clazz = ClassLoader.getSystemClassLoader().loadClass("PreparationDemo");
// 通过反射读取字段(注意:反射会触发初始化!)
// 所以不能用反射验证准备阶段的值!
// 正确方式:用 HSDB 或字节码工具查看,但面试中只需理解逻辑
System.out.println("准备阶段完成");
}
}
✅ 面试重点:
static int a = 100;在准备阶段的值是 0,不是 100!
第 4 阶段:解析(Resolution)
✅ 做了什么?
将符号引用(Symbolic Reference) 转换为直接引用(Direct Reference)。
- 符号引用 :如
"java/lang/String.valueOf:(I)Ljava/lang/String;"(字符串形式) - 直接引用:如方法在内存中的地址、偏移量
📌 解析时机:
- 静态解析 :编译期可知的(如
invokespecial、invokestatic)------在解析阶段完成。 - 动态解析 :运行时才知道的(如
invokevirtual)------延迟到真正调用时(方法表查找)。
所以解析不一定在初始化前完成!这是很多人误解的点。
第 5 阶段:初始化(Initialization)
✅ 做了什么?
执行类的 <clinit> 方法(Class Initializer),包括:
- 所有
static {}静态代码块 - 所有
static字段的显式赋值语句
<clinit>是 JVM 自动生成的,程序员无法直接编写。
📌 初始化顺序:
- 父类先于子类初始化
static字段和static{}按代码顺序执行
🧪 代码演示:
java
class Parent {
static int p = 1;
static {
System.out.println("Parent static block, p=" + p);
p = 2;
}
}
class Child extends Parent {
static int c = 3;
static {
System.out.println("Child static block, c=" + c);
c = 4;
}
}
public class InitOrder {
public static void main(String[] args) {
new Child(); // 触发初始化
}
}
输出:
Parent static block, p=1
Child static block, c=3
✅ 说明 :父类先初始化,且 static 赋值按代码顺序执行。
三、什么情况下会触发"初始化"?(7 种主动使用)
JVM 规范明确规定,只有以下 7 种情况 会触发类的初始化(从而执行 <clinit>):
new创建对象(new MyClass())- 调用类的静态方法
- 访问类的非 final 静态字段
- 使用反射(如
Class.forName("MyClass")) - 初始化一个类时,其父类尚未初始化
- 启动类(包含
main方法的类) - JDK 1.7+ 的
MethodHandle解析(动态调用)
❌ 被动使用不会触发初始化:
- 子类引用父类的静态字段(只初始化父类)
- 定义数组:
MyClass[] arr = new MyClass[10];- 访问
static final编译期常量
四、类加载器与双亲委派模型
类加载器层级:
| 加载器 | 加载路径 | 特点 |
|---|---|---|
| Bootstrap ClassLoader | $JAVA_HOME/jre/lib(如 rt.jar) |
C++ 实现,无法在 Java 中获取 |
| Extension ClassLoader | $JAVA_HOME/jre/lib/ext |
加载扩展库 |
| Application ClassLoader | -classpath 或 -cp 指定路径 |
加载应用程序类 |
| Custom ClassLoader | 自定义路径 | 如 Tomcat 的 WebAppClassLoader |
双亲委派工作流程:
java
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 1. 先检查是否已加载
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
// 2. 委托父加载器
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 3. 顶层用 Bootstrap
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ignore
}
if (c == null) {
// 4. 父加载器无法加载,自己尝试
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
为什么用双亲委派?
- 安全性 :防止用户自定义
java.lang.String替换核心类。 - 唯一性:确保核心类全局唯一,避免类冲突。
- 避免重复加载:同一个类只会被加载一次。
五、高级面试题精讲
Q1:Class.forName("X") 和 ClassLoader.loadClass("X") 有什么区别?
| 方法 | 是否初始化 | 是否触发 <clinit> |
|---|---|---|
Class.forName("X") |
是(默认) | ✅ |
ClassLoader.loadClass("X") |
否 | ❌ |
可通过
Class.forName(name, initialize, loader)控制是否初始化。
Q2:如何打破双亲委派?为什么要打破?
- 打破方式 :重写
loadClass()方法,先自己加载,再委托父类。 - 典型场景 :
- JDBC :
DriverManager使用Thread.currentThread().getContextClassLoader()加载驱动(SPI 机制)。 - Tomcat:每个 Web 应用有自己的 ClassLoader,实现应用隔离。
- JDBC :
Q3:类什么时候会被卸载?
- 条件(必须同时满足):
- 该类的所有实例都已被回收
- 加载该类的 ClassLoader 已被回收
- 该类的
java.lang.Class对象没有被任何地方引用
- 通常只在 自定义 ClassLoader + 动态加载 场景下发生(如 OSGi、热部署)。
六、总结:类加载机制的核心价值
| 阶段 | 核心作用 | 面试关键词 |
|---|---|---|
| 加载 | 获取字节码,生成 Class 对象 | ClassLoader、双亲委派 |
| 验证 | 保证字节码安全合法 | VerifyError |
| 准备 | static 字段分配内存 + 零值 | 零值 vs 赋值 |
| 解析 | 符号引用 → 直接引用 | 静态解析 vs 动态分派 |
| 初始化 | 执行 <clinit> |
主动使用、7 种触发条件 |