JVM 的类加载过程是将.class 文件(字节码文件)从磁盘加载到内存,并最终转化为 JVM 可直接使用的类对象(Class 对象) 的过程,整个过程严格遵循加载、链接、初始化 三个核心阶段,其中链接阶段又细分为验证、准备、解析三个子步骤。
一、类加载的整体流程
类加载的完整生命周期为:加载 → 验证 → 准备 → 解析 → 初始化 → 使用 → 卸载 ,其中加载、验证、准备、初始化、卸载 这五个阶段的顺序是确定的,而解析阶段则可能在初始化阶段之后执行(为了支持动态绑定 / 晚期绑定)。
二、各阶段详细解析
(一)加载(Loading)
核心目标 :将字节码文件加载到内存,并创建对应的java.lang.Class对象(类的元数据载体)。这是类加载过程的第一个阶段,由类加载器(ClassLoader) 完成,主要做三件事:
- 获取字节流
- 通过类的全限定名 (如
java.lang.String),从不同来源读取字节码文件的二进制流:- 本地文件系统(最常见,.class 文件);
- JAR/ZIP 等归档文件;
- 网络(如 Applet);
- 动态生成(如动态代理、JSP 编译);
- 数据库等其他来源。
- 通过类的全限定名 (如
- 转换存储结构
- 将读取到的字节流转换为 JVM方法区的运行时数据结构(方法区存储类的元数据,如类的版本、字段、方法、常量池等)。
- 创建 Class 对象
- 在堆内存 中创建该类的
java.lang.Class对象,作为方法区中该类元数据的访问入口(后续所有对类的操作都通过这个 Class 对象进行)。
- 在堆内存 中创建该类的
关键特性:
- 类加载器的双亲委派模型在此阶段起作用(保证类的唯一性,避免核心类被篡改)。
- 一个类的 Class 对象在 JVM 中是唯一的(同一个类加载器加载的同一个类)。
(二)链接(Linking)
链接阶段的核心目标是将加载到内存的类的元数据进行验证、准备和解析,确保其符合 JVM 规范,且能被 JVM 正确使用。
1. 验证(Verification)
核心目标 :保证字节码文件的合法性、安全性、正确性,防止恶意或错误的字节码导致 JVM 崩溃。这是链接阶段最复杂的步骤,分为四个子验证:
| 验证类型 | 验证内容 |
|---|---|
| 文件格式验证 | 检查字节码文件的魔数(0xCAFEBABE)、版本号(是否兼容当前 JVM)、常量池格式等。 |
| 元数据验证 | 检查类的继承关系(如是否继承了 final 类、是否实现了接口的所有方法)、字段 / 方法的语义合法性。 |
| 字节码验证 | 检查字节码指令的逻辑合理性(如指令执行顺序、操作数栈的类型匹配、跳转指令的合法性),防止执行非法操作。 |
| 符号引用验证 | 检查符号引用的有效性(如引用的类、方法、字段是否存在,权限是否足够)。 |
作用 :如果验证失败,会抛出VerifyError异常(如不支持的类版本号会抛出UnsupportedClassVersionError)。
2. 准备(Preparation)
核心目标 :为类的静态变量(类变量) 分配内存,并设置默认初始值(非程序员指定的初始值)。
关键细节:
-
分配内存的位置 :JDK 7 及之前,静态变量存储在方法区 ;JDK 8 及之后,方法区被元空间(Metaspace)替代,静态变量移至堆内存(Class 对象的一部分)。
-
默认初始值规则 :
数据类型 默认初始值 byte/short/int 0 long 0L float 0.0f double 0.0d boolean false char '\u0000'(空字符) 引用类型 null -
特殊情况 :如果静态变量被
final修饰(编译期常量),则在准备阶段直接赋值为程序员指定的常量值(如public static final int NUM = 10;,准备阶段 NUM 就会被赋值为 10),而非默认值。
示例:
java
public class Test {
public static int a = 10; // 准备阶段a被赋值为0,初始化阶段才会赋值为10
public static final int b = 20; // 准备阶段b直接赋值为20
}
3. 解析(Resolution)
核心目标 :将常量池中的符号引用 转换为直接引用。
- 符号引用 :以字符串形式表示的类、方法、字段的引用(如
java.lang.String、add()V),不直接指向内存地址。 - 直接引用:指向内存中实际对象的指针、偏移量或句柄(可以直接定位到目标的内存地址)。
解析的主要对象包括:
- 类或接口的解析:将符号引用的类名转换为对应的 Class 对象的直接引用。
- 字段的解析:将符号引用的字段名转换为对应字段在类中的内存偏移量。
- 方法的解析:将符号引用的方法名转换为对应方法的直接引用(静态方法、私有方法等非虚方法在此阶段完成解析,虚方法则在运行时通过动态分派解析)。
注意 :解析阶段并非必须在准备阶段后立即执行,JVM 可以根据需要延迟解析(如在首次调用方法时才解析),这为动态绑定(晚期绑定) 提供了支持。
(三)初始化(Initialization)
核心目标 :执行类的静态代码块(static {}) 和为静态变量 赋值为程序员指定的初始值,这是类加载过程中唯一由程序员编写的代码执行的阶段。
1. 初始化的触发条件(主动使用)
JVM 规范规定,只有当类被主动使用时才会触发初始化,被动使用不会触发。主动使用的场景包括:
- 创建类的实例(
new Test()); - 调用类的静态方法(
Test.staticMethod()); - 访问类的静态变量(非 final 修饰的,如
Test.a); - 反射调用(
Class.forName("com.test.Test")); - 初始化一个类的子类(子类初始化时,父类会先被初始化);
- JVM 启动时的主类(包含
main()方法的类); - 调用 JDK 7 及以上的
java.lang.invoke.MethodHandle实例(且该实例指向的方法是静态方法)。
被动使用的场景(不触发初始化):
- 访问类的静态常量(
final修饰的编译期常量,如Test.b); - 通过子类访问父类的静态变量(仅触发父类初始化,子类不初始化);
- 数组创建(
Test[] arr = new Test[10];,仅创建数组对象,不初始化 Test 类); - 类加载器的
loadClass()方法加载类(仅触发加载,不触发初始化)。
2. 初始化的执行顺序
初始化阶段严格按照以下顺序执行:
- 父类的初始化:如果类有父类且父类未被初始化,则先初始化父类(保证父类的静态代码和静态变量先执行)。
- 静态变量的赋值:按代码编写顺序为静态变量赋值。
- 静态代码块的执行 :按代码编写顺序执行静态代码块(静态变量赋值和静态代码块在编译时会被合并为
<clinit>()方法)。
关键特性:
- 类的
<clinit>()方法(类初始化方法):由编译器自动收集类中的静态变量赋值语句和静态代码块组成,只会被 JVM 执行一次(保证类只初始化一次)。 - 如果类中没有静态变量赋值和静态代码块,则编译器不会生成
<clinit>()方法。 <clinit>()方法不需要显式调用,由 JVM 在初始化阶段自动执行。
示例:
java
class Parent {
public static int parentVar = 10;
static {
System.out.println("Parent静态代码块");
}
}
class Child extends Parent {
public static int childVar = 20;
static {
System.out.println("Child静态代码块");
}
}
public class Test {
public static void main(String[] args) {
System.out.println(Child.childVar);
// 输出顺序:
// Parent静态代码块
// Child静态代码块
// 20
}
}
三、类加载器与双亲委派模型
类加载的过程由类加载器完成,JVM 的类加载器体系遵循双亲委派模型,这是保证类加载安全和唯一性的核心机制。
-
类加载器的分类:
- 启动类加载器(Bootstrap ClassLoader) :由 C++ 实现,负责加载
$JAVA_HOME/jre/lib下的核心类库(如 rt.jar)。 - 扩展类加载器(Extension ClassLoader) :负责加载
$JAVA_HOME/jre/lib/ext下的扩展类库。 - 应用程序类加载器(Application ClassLoader):负责加载用户类路径(ClassPath)下的类。
- 自定义类加载器 :用户继承
ClassLoader类实现的类加载器。
- 启动类加载器(Bootstrap ClassLoader) :由 C++ 实现,负责加载
-
双亲委派模型的工作机制:
- 当一个类加载器收到加载请求时,首先将请求委派给父类加载器加载;
- 只有当父类加载器无法加载该类时(在其搜索范围内找不到该类),才由当前类加载器自己加载。
四、类的卸载
类的卸载是指将类的元数据从方法区(元空间)中移除,同时销毁对应的 Class 对象。JVM 对类的卸载有严格限制:
- 只有自定义类加载器加载的类才可能被卸载(启动类加载器加载的核心类永远不会被卸载);
- 当一个类的所有实例都被回收,且对应的类加载器也被回收时,该类才可能被卸载。
五、总结
JVM 类加载过程是一个分层、有序、安全的过程:
- 加载:把字节码读入内存,创建 Class 对象;
- 链接:验证合法性、准备静态变量、解析符号引用;
- 初始化:执行静态代码,完成静态变量的赋值。
这个过程保证了类在 JVM 中被正确加载和使用,同时通过双亲委派模型、验证阶段等机制保障了 JVM 的安全性和稳定性。