Java 类加载是 Java 虚拟机(JVM)在运行时动态加载 .class
文件到内存的过程。这个过程和类的生命周期紧密相关。我们来系统地学习下 Java 类加载的 过程、生命周期阶段、类加载器的作用 以及 相关细节和面试点。
🧩 一、类加载过程(Class Loading Process)
Java 类从 .class
文件变成 JVM 可执行的对象代码,会经过 七个步骤,可以分为三大阶段:
✅ 加载 -> 连接 -> 初始化:
加载(Loading)↓
验证(Verification)↓
准备(Preparation)↓
解析(Resolution)↓
初始化(Initialization)↓
使用(Using)↓
卸载(Unloading)
🧠 二、七大生命周期阶段详解:
1. 加载(Loading)
- 干什么: 找到
.class
文件并读取其字节流。 - 由谁干: 类加载器(ClassLoader)。
- 从哪来: 本地磁盘、网络、JAR 包等。
- 结果: 生成
java.lang.Class
类的实例。
2. 验证(Verification)
- 干什么: 验证字节码是否合法、符合 JVM 规范。
- 为什么要: 防止恶意或错误的类破坏 JVM 安全。
- 检查什么: 魔数、常量池、方法结构、控制流。
3. 准备(Preparation)
- 干什么: 为类的静态变量分配内存,并设置默认初始值(不会执行初始化赋值)。
-
static int a = 5; // 在这阶段,a 分配了内存,值为 0,不是 5
4. 解析(Resolution)
- 干什么: 将常量池中的符号引用(Symbolic Reference)替换成直接引用(Direct Reference)。
- 如: 把类名
"java/lang/Object"
转成真实的类对象。 - ⚠️:可能在运行时延迟解析(lazy resolve)。
5. 初始化(Initialization)
- 干什么: 执行类中的
<clinit>()
方法(静态代码块和静态变量初始化)。 - 初始化顺序:
- 父类先初始化;
- 子类再初始化;
- 静态变量和静态代码块按出现顺序执行。
6. 使用(Using)
- 实例化对象、调用方法、访问字段 都属于使用阶段。
7. 卸载(Unloading)
- 何时: 某个类对应的
ClassLoader
被 GC 回收,并且该类无任何实例。 - 注意: JVM 很少卸载类,通常用于动态模块或热部署。
🎯 三、类加载器(ClassLoader)
Java 使用双亲委派模型:
BootstrapClassLoader(引导类加载器)↑
ExtClassLoader(扩展类加载器)↑
AppClassLoader(应用类加载器)↑
自定义ClassLoader(如 SPI、插件等)
🚀 加载器加载顺序(查找顺序):
- 请求父加载器先加载;
- 如果父加载器找不到,再由当前类加载器尝试加载;
- 保证核心类不会被覆盖(比如你写一个
java.lang.String
也不会被加载)。
📌 四、常见类加载时机(何时触发加载)
- new 对象时
- 访问静态字段或方法
- 使用 Class.forName()
- 子类初始化时会先初始化父类
- 反射调用类成员时
- 启动主类时
❗ 五、面试重点&易错点:
点 | 内容 |
---|---|
类何时初始化? | 有静态变量/静态代码块时且被访问 |
类何时不会初始化? | 引用数组不会触发类初始化 |
<clinit>() 和构造器区别? | <clinit>() 是类初始化,构造器是对象初始化 |
自定义 ClassLoader 作用? | 动态加载模块、热更新、插件系统 |
双亲委派有什么好处? | 安全、避免重复加载 |
🧪 小练习题:
class A {static {System.out.println("A static block");}
}
public class Test {public static void main(String[] args) {A[] arr = new A[10]; // 会不会输出 A static block?}
}
✅ 不会,因为只是创建了数组,没有初始化类 A
。
如果你想我配张图解释类加载的阶段,或者深入讲讲某个阶段的底层原理(比如常量池解析),我也可以继续展开。要不要?