JVM类加载过程
JVM类加载过程是将类的字节码文件(.class
)加载到内存,并转换为运行时数据结构的过程,核心分为加载(Loading)、链接(Linking)、初始化(Initialization)三个阶段,其中链接又包含验证、准备、解析三个子阶段。以下是详细流程:
1. 加载(Loading)
- 任务:查找并加载类的二进制数据。
- 过程:
- 通过类的全限定名(如
com.example.MyClass
)获取字节码。 - 将字节码解析为方法区(元空间)的运行时数据结构。
- 在堆中创建该类的
java.lang.Class
对象,作为访问方法区数据的入口。
- 通过类的全限定名(如
- 类加载器:
- Bootstrap ClassLoader:加载JRE核心库(
rt.jar
等),C++实现。 - Extension ClassLoader:加载扩展库(
jre/lib/ext
目录)。 - Application ClassLoader:加载用户类路径(ClassPath)下的类。
- 自定义ClassLoader:用户可继承
ClassLoader
实现自定义加载逻辑。
- Bootstrap ClassLoader:加载JRE核心库(
2. 链接(Linking)
(1) 验证(Verification)
- 确保字节码合法且符合JVM规范:
- 文件格式验证:检查魔数(
0xCAFEBABE
)、版本号等。 - 元数据验证:检查继承、方法重写等语义(如是否实现抽象方法)。
- 字节码验证:分析代码逻辑(如操作数栈类型匹配)。
- 符号引用验证:检查引用的类/方法/字段是否存在(发生在解析阶段)。
- 文件格式验证:检查魔数(
(2) 准备(Preparation)
- 为类变量(静态变量) 分配内存并设置默认初始值(非显式赋值):
static int value = 123; // 准备阶段 value = 0,而非123
- 常量(
static final
)在此阶段直接赋值:
static final int CONST = 123; // 准备阶段 CONST = 123
- 常量(
(3) 解析(Resolution)
- 将常量池中的符号引用替换为直接引用:
- 符号引用:用字符串描述引用的目标(如
java/lang/Object
)。 - 直接引用:指向目标在内存中的指针、偏移量等。
- 符号引用:用字符串描述引用的目标(如
3. 初始化(Initialization)
- 执行类构造器
<clinit>()
:- 为类变量赋显式初始值(如
static int value = 123;
)。 - 执行静态代码块(
static {}
)。
- 为类变量赋显式初始值(如
- 触发条件(首次主动使用类时):
- 创建实例(
new
)、访问静态变量/方法。 - 反射调用(
Class.forName()
)、初始化子类等。
- 创建实例(
- 线程安全:JVM保证
<clinit>()
的同步执行。
4. 使用(Using)
- 类完成初始化后,可正常创建对象、调用方法、访问字段。
5. 卸载(Unloading)
- 条件:类的
Class
对象无引用,且无存活实例。 - 由GC回收方法区(元空间)数据。
关键特性
-
双亲委派模型:
- 类加载请求先委派给父加载器处理。
- 避免重复加载,保证核心类安全(如用户无法自定义
java.lang.String
)。
-
惰性加载:
- 类在首次“主动使用”时才初始化(如
new
、访问静态字段等)。
- 类在首次“主动使用”时才初始化(如
-
类加载示例:
public class Main {public static void main(String[] args) {System.out.println(Child.value); // 父类初始化,子类不初始化} } class Parent {static int value = 10;static { System.out.println("Parent init!"); } } class Child extends Parent {static { System.out.println("Child init!"); } } // 输出:Parent init! 10
常见问题
ClassNotFoundException
:类加载器找不到类定义。NoClassDefFoundError
:编译时存在类,运行时缺失。- 打破双亲委派:如JDBC通过
Thread.currentThread().setContextClassLoader()
实现SPI加载。
理解类加载过程对解决类冲突、热部署、模块化开发等场景至关重要。