JVM 类加载机制
基于 JDK 8+ 的 JVM 类加载机制深度解析。
一、类加载概述

1.1 什么是类加载
类加载是指将 .class 文件中的字节码读入内存,并为之创建对应的运行时数据结构的过程。
1.2 类加载的产物
类加载完成后会产生两个核心数据结构:
| 产物 | 存储位置 | 作用 |
|---|---|---|
| Klass 结构 | 元空间 | 类的完整元数据(给 JVM 内部使用) |
| Class 对象 | 堆 | 反射入口 + 静态变量存储(给 Java 代码使用) |
元空间 堆
┌────────────┐ ┌─────────────────────┐
│ Klass │ │ Class 对象 │
│ │◄──────────────────│ │
│ 字段定义 │ _java_mirror │ 静态变量值 │
│ 方法定义 │──────────────────►│ 反射能力 │
│ 常量池 │ klass pointer │ │
│ vtable │ │ │
└────────────┘ └─────────────────────┘
两者通过双向指针互相引用二、类加载的三个阶段
┌─────────────────────────────────────────────────────────────────────────────────────┐
│ 类加载过程 │
├─────────────────────────────────────────────────────────────────────────────────────┤
│ │
│ 1. 加载 (Loading) │
│ │
│ 2. 链接 (Linking) │
│ ├── 验证 (Verification) │
│ ├── 准备 (Preparation) │
│ └── 解析 (Resolution) │
│ │
│ 3. 初始化 (Initialization) │
│ │
└─────────────────────────────────────────────────────────────────────────────────────┘2.1 加载(Loading)
做什么:
- 通过类的全限定名获取定义此类的二进制字节流
- 将字节流代表的静态存储结构转化为元空间的运行时数据结构(Klass)
- 在堆中创建该类的
java.lang.Class对象
Demo.class 文件
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 类加载器读取字节流 │
│ │
│ - Bootstrap ClassLoader(核心类库) │
│ - Extension ClassLoader(扩展类库) │
│ - Application ClassLoader(应用类路径) │
└─────────────────────────────────────────────────────────────────┘
│
├───────────────────────────────────────┐
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ 元空间 │ │ 堆 │
│ │ │ │
│ Klass 结构 │◄────────────────►│ Class 对象 │
│ │ │ │
└─────────────────┘ └─────────────────┘2.2 链接(Linking)
链接分为三个子阶段:
2.2.1 验证(Verification)
做什么:确保 Class 文件的字节流符合 JVM 规范,不会危害 JVM 安全。
| 验证类型 | 内容 |
|---|---|
| 文件格式验证 | 魔数 CAFEBABE、版本号、常量池等 |
| 元数据验证 | 语义分析:是否有父类、是否继承了 final 类等 |
| 字节码验证 | 数据流和控制流分析,确保语义合法 |
| 符号引用验证 | 确保解析能正确执行 |
2.2.2 准备(Preparation)
做什么:为静态变量分配内存并设置零值。
class Demo {
static int count = 10; // 准备阶段:count = 0
static final int MAX = 100; // 准备阶段:MAX = 100(编译期常量直接赋值)
static final Object LOCK = new Object(); // 准备阶段:LOCK = null
}| 静态变量类型 | 准备阶段的值 |
|---|---|
static int count = 10 | 0 |
static final int MAX = 100 | 100(编译期常量,直接赋值) |
static final Object LOCK = new Object() | null(运行时才能确定) |
注意:这里只处理静态变量,不处理实例变量!实例变量在创建对象时才处理。为什么呢?因为我们的类加载机制,重点的目的是创建对应类的一个 kclass 元数据信息,以及在堆中创建对应的一个 Class 对象。这里我们知道 Class 对象只负责了静态变量的存储,并不会持有实例变量,实例变量是实例对象各自拥有的,因此我们在这个类加载阶段,无论涉及到的赋值还会初始化,都跟实例变量无关,实例变量的赋值和初始化要等到实例对象创建时才进行。!!!!
2.2.3 解析(Resolution)
做什么:将常量池中的符号引用替换为直接引用。
符号引用 直接引用
(字符串描述) (内存地址/偏移量)
"com/example/Demo" → Klass 地址 0x7f8a2b3c
"count:I" → 字段偏移量 12
"sayHello:()V" → 方法入口地址解析的四种符号引用类型:
| 类型 | 解析前 | 解析后 |
|---|---|---|
| 类或接口 | "com/example/Demo" | Klass 地址 |
| 字段 | "Demo.count:I" | 字段偏移量 |
| 方法 | "Demo.sayHello:()V" | 方法入口地址 / vtable 索引 |
| 接口方法 | "Interface.method:()V" | itable 索引 |
解析时机:
- 立即解析(Eager):类加载时解析所有符号引用
- 延迟解析(Lazy):用到时才解析(HotSpot 默认)
理解:对于这个解析来说,主要就是将我们读到元空间中的一个 kclass 对象,将其中的符号引用,转换为直接引用。例如,我在 Main 类中使用 Demo 类,那么在 Main 类加载时,会解析 Demo 类的符号引用,将 Demo 类的符号引用转换为直接引用。这里的符号引用解析,具体是去 JVM 维护的一个类加载表中,找到 Demo 类的 kclass 对象,然后将其中的符号引用,转换为直接引用。主要的一个返回值,就是返回 Demo 类在元空间中的一个 kclass 对象的地址。这个地址,就是 Demo 类的直接引用。这个直接引用,会被存储在 Main 类的 kclass 对象中。
2.3 初始化(Initialization)
做什么:执行类构造器 <clinit> 方法。
<clinit> 方法由编译器自动收集以下内容生成:
- 静态变量的赋值语句
- 静态代码块
static { }
class Demo {
static int count = 10;
static {
System.out.println("静态代码块");
count = 20;
}
}
// <clinit> 方法内容(编译器生成):
// count = 10;
// System.out.println("静态代码块");
// count = 20;初始化顺序:父类的 <clinit> 先于子类执行。
三、Klass 与 Class 对象

3.1 Klass 结构(元空间)
Klass 是 JVM 内部使用的 C++ 对象,存储类的完整元数据:
┌─────────────────────────────────────────────────────────────────────────────────────┐
│ Klass 结构 │
├─────────────────────────────────────────────────────────────────────────────────────┤
│ │
│ _java_mirror 指向堆中的 Class 对象 │
│ _super 指向父类的 Klass │
│ _layout_helper 对象大小信息 │
│ │
│ 运行时常量池 符号引用 / 已解析的直接引用 │
│ 字段表 字段名、类型、偏移量 │
│ 方法表 方法名、签名、字节码地址 │
│ vtable 虚方法表(用于多态) │
│ │
└─────────────────────────────────────────────────────────────────────────────────────┘3.2 Class 对象(堆)
Class 对象是 Java 对象,给程序员使用:
| 功能 | 说明 |
|---|---|
| 反射入口 | 获取类信息、动态创建对象、调用方法 |
| 静态变量存储 | 静态字段的值存储在 Class 对象中 |
| 类型判断 | instanceof、类型转换检查 |
Class<?> clazz = Demo.class;
// 反射操作
clazz.getName();
clazz.getDeclaredFields();
clazz.getDeclaredMethods();
clazz.newInstance();3.3 两者的关系
元空间 堆
┌────────────────┐ ┌────────────────────────┐
│ Klass │ │ Class 对象 │
│ │ │ │
│ _java_mirror ─┼─────────────────►│ │
│ │ │ klass pointer ───────┼──┐
│ │◄─────────────────┼─────────────────────────┘ │
│ │ │ │ │
│ 真正的模板 │ │ 反射入口 │ │
│ 决定对象大小 │ │ 静态变量值 │ │
│ │ │ │ │
└────────────────┘ └────────────────────────┘ │
▲ │
└────────────────────────────────────────────────────────┘四、运行时常量池
4.1 常量池的演变
【编译期】.class 文件中的常量池
│
│ 存储符号引用(字符串形式):
│ - "java/lang/Object"
│ - "println"
│ - "(Ljava/lang/String;)V"
│
▼ 类加载
【运行时】Klass 中的运行时常量池
存储:
- 未解析的:符号引用(字符串)
- 已解析的:直接引用(内存地址、偏移量)4.2 每个类一份
运行时常量池是每个类独立一份,存储在该类的 Klass 结构中,不是全局共享的。
Demo 的 Klass
┌─────────────────────────────────┐
│ Demo 的运行时常量池 │
│ │
│ #1 = "java/lang/Object" │
│ #2 = Klass* 0x7f001000 (已解析) │
│ ... │
└─────────────────────────────────┘
Main 的 Klass
┌─────────────────────────────────┐
│ Main 的运行时常量池 │
│ │
│ #1 = "com/example/Demo" │
│ #2 = Klass* 0x7f8a2b3c (已解析) │
│ ... │
└─────────────────────────────────┘五、系统字典
5.1 什么是系统字典
JVM 内部维护一个系统字典(System Dictionary),用于存储所有已加载的类。
┌─────────────────────────────────────────────────────────────────────────────────────┐
│ 系统字典(哈希表) │
├─────────────────────────────────────────────────────────────────────────────────────┤
│ │
│ Key = 类的全限定名 + 类加载器 │
│ Value = Klass 指针 │
│ │
│ ┌─────────────────────────────────────────┬────────────────────────────────────┐ │
│ │ Key │ Value │ │
│ ├─────────────────────────────────────────┼────────────────────────────────────┤ │
│ │ ("java/lang/Object", BootstrapLoader) │ Klass* 0x7f001000 │ │
│ │ ("java/lang/String", BootstrapLoader) │ Klass* 0x7f002000 │ │
│ │ ("com/example/Demo", AppClassLoader) │ Klass* 0x7f8a2b3c │ │
│ │ ... │ ... │ │
│ └─────────────────────────────────────────┴────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────────────┘5.2 为什么 Key 包含类加载器
同一个类被不同的类加载器加载,是不同的类!
// ClassLoaderA 加载的 Demo
Demo d1 = ...;
// ClassLoaderB 加载的 Demo
Demo d2 = ...;
// 虽然类名相同,但它们是不同的类
d1 instanceof Demo(ClassLoaderB版本) → false5.3 解析时如何查找
解析符号引用 "com/example/Demo" 时:
- 确定使用哪个类加载器(通常是当前类的加载器)
- 在系统字典中查找
(类名, 类加载器) - 找到 → 返回 Klass 地址
- 没找到 → 触发类加载 → 加载后注册到系统字典
六、双亲委派机制

6.1 类加载器层次
BootstrapClassLoader(C++ 实现)
│
│ 加载 rt.jar 等核心类库
│
▼
ExtensionClassLoader
│
│ 加载 ext 目录下的扩展类库
│
▼
ApplicationClassLoader
│
│ 加载 classpath 下的应用类
│
▼
自定义 ClassLoader6.2 双亲委派过程
加载 com.example.Demo
│
▼
ApplicationClassLoader
│ 委派给父加载器
▼
ExtensionClassLoader
│ 委派给父加载器
▼
BootstrapClassLoader
│
│ 尝试加载 → 加载不到
│
▼ 返回给子加载器
ExtensionClassLoader
│ 尝试加载 → 加载不到
│
▼ 返回给子加载器
ApplicationClassLoader
│ 尝试加载 → 找到 Demo.class → 加载成功
▼
加载完成6.3 为什么使用双亲委派
- 安全性:防止核心类被篡改(如自定义 java.lang.Object)
- 避免重复加载:父加载器加载过的类,子加载器不会再加载
七、类加载时机
7.1 主动引用(触发初始化)
| 场景 | 说明 |
|---|---|
| new 对象 | new Demo() |
| 访问静态字段 | Demo.count(非 final 常量) |
| 调用静态方法 | Demo.doSomething() |
| 反射 | Class.forName("Demo") |
| 初始化子类 | 先初始化父类 |
| 主类 | 启动时包含 main 方法的类 |
7.2 被动引用(不触发初始化)
| 场景 | 说明 |
|---|---|
| 通过子类引用父类静态字段 | SubDemo.parentField(只初始化父类) |
| 数组定义 | Demo[] arr = new Demo[10] |
| 引用 final 常量 | Demo.MAX(编译期已内联) |
八、<clinit> vs <init>
<clinit> | <init> | |
|---|---|---|
| 含义 | 类构造器 | 实例构造器 |
| 触发时机 | 类加载初始化阶段 | new 对象时 |
| 执行次数 | 每个类只执行一次 | 每次 new 都执行 |
| 处理内容 | 静态变量赋值 + 静态代码块 | 实例变量赋值 + 实例代码块 + 构造方法 |
class Demo {
static int count = 10; // <clinit>
static { ... } // <clinit>
int age = 18; // <init>
{ ... } // <init>
Demo() { ... } // <init>
}九、常见面试问题
| 问题 | 要点 |
|---|---|
| 类加载的三个阶段? | 加载 → 链接(验证、准备、解析)→ 初始化 |
| 类加载的产物? | 元空间的 Klass + 堆的 Class 对象 |
| 准备阶段做什么? | 静态变量分配内存 + 赋零值 |
| 初始化阶段做什么? | 执行 <clinit>,静态变量赋真值 + 静态代码块 |
| 什么是符号引用? | 字符串形式的描述(如类名、方法名) |
| 什么是直接引用? | 内存地址、偏移量、索引 |
| Klass 和 Class 对象的区别? | Klass 是模板(JVM 用),Class 是反射入口(程序员用) |
| 系统字典是什么? | 存储已加载类的哈希表,Key = 类名 + 类加载器 |
| 双亲委派的作用? | 安全性 + 避免重复加载 |
| 什么时候触发类初始化? | new、访问静态成员、反射、子类初始化、主类 |
