下面从 类加载的五个阶段 逐步分析 JVM 在每个阶段的内存变化 。
(以 HotSpot + Java17 为例,Java8 以后方法区已改为 Metaspace)
JVM 相关内存区域速览
区域 | 作用 | 是否线程共享 |
---|---|---|
堆(Heap) | 对象实例、数组 | 共享 |
方法区 / 元空间 (Metaspace) | 类的元信息、静态变量、常量池、JIT 代码 | 共享 |
运行时常量池 | 字面量、符号引用 | 共享 |
栈 (Java Stack) | 局部变量、操作数栈、方法帧 | 线程私有 |
程序计数器 | 当前字节码行号 | 线程私有 |
类加载生命周期与内存变化
1️⃣ 加载 (Loading)
做的事
- 通过 ClassLoader 读取
.class
字节码,形成Class
对象。
内存变化
- Metaspace:分配空间存放类的结构信息(字段、方法表、字节码指令)。
- 运行时常量池:复制 class 文件中的常量池(符号引用、字符串字面量等)。
- 堆/栈:暂时无变化。
2️⃣ 验证 (Verification)
做的事
- 校验字节码格式、安全性(魔数、常量池索引、数据流、控制流)。
内存变化
- Metaspace 中的类结构已存在,只进行一致性检查,没有额外分配大量内存。
3️⃣ 准备 (Preparation)
做的事
- 为类的静态变量分配内存并设默认值(int=0、reference=null 等)。
内存变化
-
Metaspace:
- 分配静态变量所需空间,存储在类元数据区域。
- 如果是
static final
且编译期常量,值直接存入运行时常量池。
-
堆:此时不会创建对象实例。
4️⃣ 解析 (Resolution)
做的事
- 把运行时常量池中的符号引用解析成直接引用(内存地址或句柄)。
内存变化
- 运行时常量池中保存的直接引用指针被填充。
- 可能触发其他类的加载/初始化,间接扩展 Metaspace 占用。
5️⃣ 初始化 (Initialization)
做的事
- 执行
<clinit>
:静态变量显式赋值、静态代码块。
内存变化
-
Metaspace:静态变量值被正式写入。
-
堆:
- 如果静态变量是对象或集合,此时会在堆上创建对象实例并把引用放到 Metaspace 静态变量表。
-
栈 :执行
<clinit>
方法帧入栈,执行完后出栈。
运行时继续变化
- 实例化对象 → 在堆上分配实例内存。
- 类卸载(ClassLoader 不再被引用)→ 其在 Metaspace 中的元数据、静态变量会被 GC 回收。
简化时序图
Class 文件
│
▼
[加载] ──► Metaspace: 类结构、常量池
│
[验证] ──► 仅检查,无大变化
│
[准备] ──► Metaspace: 静态变量默认值
│
[解析] ──► 常量池符号引用 → 直接引用
│
[初始化] ──► Metaspace: 赋真实值
│ Heap: 静态对象实例
小结
- 类元信息 + 静态变量存放在 Metaspace
- 实例对象在 Heap
- 执行方法时用栈
- 类加载阶段的内存主要变化是 Metaspace 的增长 ,初始化阶段可能会触发 Heap 分配。