HotSpot 内存区域详解
1. HotSpot 概述
1.1 HotSpot 是什么
HotSpot 是 Oracle JDK 和 OpenJDK 的默认 JVM 实现,也是最广泛使用的 JVM 实现。其名称来源于 JIT 编译器的热点检测技术。
1.2 HotSpot 的设计理念
- 自适应优化:通过运行时性能分析找出热点代码
- 混合执行模式:解释执行 + JIT 编译
- 分代垃圾收集:针对不同生命周期的对象使用不同的 GC 算法
2. 元空间
2.1 元空间的内存结构
元空间(Metaspace)是 HotSpot 对方法区的实现,位于本地内存中。
类加载器命名空间
类加载器 A
元数据
类加载器 B
元数据
类加载器 C
元数据
元空间 (Metaspace)
2.2 元空间的组成
| 组成部分 | 说明 |
|---|---|
| 类元数据 | 类的名字、修饰符、父类、接口等 |
| 方法字节码 | 方法的字节码指令 |
| 方法表 | 虚方法表(vtable)和接口方法表(itable) |
| 常量池 | 类的常量池 |
| 注解数据 | 类、方法、字段的注解 |
2.3 元空间的分配机制
元空间使用 Chunk 链表的方式进行内存管理。
Chunk 链表
类加载器
Chunk 1
已满
Chunk 2
当前使用
Chunk 3
待分配
分配流程
类加载请求
当前 chunk
有空间
直接分配
当前 chunk
空间不足
向操作系统
申请新 chunk
检查是否触及
水位线
未触及水位线
触及水位线
触发堆 GC
GC 完成
调整水位线
分配成功
继续分配
达到 MaxMetaspaceSize
抛出 OutOfMemoryError
分配新类
当前Chunk
申请新Chunk
申请新内存
检查水位线
分配成功
触发GC并分配
调整水位线
OOM
关键机制
-
Chunk 链表管理
- 每个 ClassLoader 维护一个 chunk 链表
- 当前 chunk 剩余空间不足时,申请新 chunk
- 旧 chunk 不会释放(只在整个 ClassLoader 回收时释放)
-
水位线机制
- 元空间使用量触及水位线时触发 GC
- GC 后无论是否回收够空间,都会继续分配
- 同时调整水位线(降低触发 GC 的频率)
- 直到达到 MaxMetaspaceSize 才抛出 OOM
-
以 ClassLoader 为单位
- 每个 ClassLoader 有独立的 chunk 链表
- ClassLoader 卸载时,其所有 chunk 一次性释放
2.4 元空间的容量管理
| 参数 | 说明 | 默认值 |
|---|---|---|
-XX:MetaspaceSize |
元空间初始大小 | 取决于平台(约21MB) |
-XX:MaxMetaspaceSize |
元空间最大大小 | 无限制 |
-XX:CompressedClassSpaceSize |
压缩类空间大小 | 1GB |
-XX:MinMetaspaceFreeRatio |
最小空闲比例 | 40% |
-XX:MaxMetaspaceFreeRatio |
最大空闲比例 | 95% |
2.5 元空间的回收机制
元空间没有独立的 GC
重要概念:元空间本身不运行 GC,元空间的回收完全依赖于堆的 GC。
回收机制
元空间回收只有一种方式:堆的 GC 卸载 ClassLoader。
回收有两种触发场景:
场景一:正常的堆 GC(主要机制)
堆 GC 运行
各种原因触发
检查 ClassLoader
可达性
卸载不可达的
ClassLoader
释放对应的
元空间内存
这是元空间回收的主要机制。堆 GC 运行时会检查 ClassLoader 的可达性,卸载不可达的 ClassLoader,并释放对应的元空间内存。
场景二:触及水位线(附加触发)
是
否
申请新 Chunk
触及
水位线
触发堆 GC
继续分配
堆 GC 运行
调整水位线
继续分配
触及水位线时,会触发一次堆 GC,增加 ClassLoader 卸载的机会。
关键点:
- 回收起点是堆 GC(卸载 ClassLoader)
- 触及水位线会触发额外的 GC
- GC 后无论是否回收空间,都会继续分配并调整水位线
GC 类型
元空间触及水位线时触发的 GC 类型取决于当前使用的 GC:
| GC 类型 | 触发的 GC | 说明 |
|---|---|---|
| Serial GC | Full GC | |
| Parallel GC | Full GC | |
| G1 GC | Full GC | |
| ZGC | 并发周期 | 并发的 GC 周期 |
GC 与堆的关系
-
元空间满了不会触发堆 GC
- 元空间容量不足时,不会因为"空间不足"而触发堆 GC
- 只会触及水位线时才触发堆的 GC
-
堆满了不会扫描元空间
- 堆 GC 时只扫描堆内的对象引用
- 不会扫描元空间内的元数据
- 这是 JDK 8 相比 JDK 7 的重大改进
-
独立的水位线机制
- 元空间有自己的水位线
- 触及水位线时会顺带触发堆的 GC(尝试类卸载)
- 但这只是为了回收类元数据,不是为了回收堆的对象
2.6 相比 JDK 7 永久代的优势
| 对比项 | JDK 7 永久代 | JDK 8 元空间 |
|---|---|---|
| GC 时需要扫描 | 堆 + 永久代 | 只扫描堆 |
| GC 时需要移动 | 永久代内的对象 | 不需要移动元数据 |
| 引用维护 | 需要维护永久代内的对象引用 | 不需要维护 |
| GC 复杂度 | 高(需要遍历永久代对象图) | 低(只检查 ClassLoader 可达性) |
| 扫描位置 | 堆 + 永久代 | 只扫描堆 |
| 相互影响 | 堆满扫描永久代(效率低) | 完全独立 |
优势总结
-
不需要扫描元空间内的引用关系
- 永久代:GC 时需要扫描永久代内的对象引用关系
- 元空间:只需要检查 ClassLoader 是否被堆引用
-
不需要移动元空间内的对象
- 永久代:GC 时可能需要移动和整理永久代内的对象
- 元空间:元数据不需要移动
-
不需要维护元空间内的对象引用
- 永久代:需要维护对象之间的引用关系
- 元空间:引用关系在堆中,元空间只存储元数据
-
GC 效率更高
- 永久代:堆满时还要扫描永久代,效率低
- 元空间:堆和元空间完全独立,互不影响
3. 堆
3.1 堆的内存布局
重要说明:不同 GC 的堆内存布局差异很大,本节展示的是 Serial GC 和 Parallel GC 的经典分代布局。
3.1.1 Serial GC 和 Parallel GC 的布局
这两种 GC 采用传统的分代设计,将对象按生命周期分为新生代和老年代。
新生代 (Young Generation)
分配新对象
GC后存活
GC后存活
GC后多次存活
Java 堆
Eden 区
Survivor 0 (From)
Survivor 1 (To)
老年代 (Old Generation)
3.1.2 G1 GC 的布局
G1 GC 抛弃了传统的物理分代,将堆划分为多个大小相等的 Region(通常 2048 个),Region 可以动态分配角色。
Region (约 2048 个)
Eden Region
Eden Region
Survivor Region
Survivor Region
Old Region
Old Region
Humongous Region
大对象
G1 堆
由多个 Region 组成
G1 GC 的特点:
- 不再有物理上的 Eden、Survivor、老年代区域
- Region 可以动态切换角色(Eden、Survivor、Old)
- Humongous Region 用于存储超过半个 Region 大小的对象
- 逻辑上仍然是分代的,但物理上基于 Region
3.1.3 ZGC 的布局
ZGC 从 JDK 21 开始采用分代设计(分代 ZGC),基于 Region 区分新生代和老年代。
老年代 Region
ZGC 老年代 Region 1
ZGC 老年代 Region 2
ZGC 老年代 Region 3
新生代 Region
ZGC 新生代 Region 1
ZGC 新生代 Region 2
ZGC 新生代 Region 3
ZGC 堆
分代设计
ZGC 的特点:
- 采用分代设计(JDK 21+)
- 基于 Region 的布局,Region 区分新生代和老年代角色
- 使用染色指针和读屏障实现并发 GC
- 全堆并发处理,STW(Stop-The-World)时间极短(通常在 1ms 以内)
3.1.4 不同 GC 堆内存布局对比
| 特性 | Serial / Parallel GC | G1 GC | ZGC (JDK 21+) |
|---|---|---|---|
| 分代策略 | 物理分代 | 逻辑分代 | 分代 |
| 内存布局 | 固定区域(Eden、Survivor、Old) | 动态 Region | 动态 Region(区分新生代/老年代) |
| Region 数量 | 无 Region 概念 | 约 2048 个 | 多个 Region |
| 大对象处理 | 直接进入老年代 | Humongous Region | 老年代 Region |
| 对象移动 | 跨代移动 | Region 间移动 | Region 间移动 |
| GC 类型 | Minor GC、Full GC | Young GC、Mixed GC | 新生代 GC、老年代 GC |
| 并发处理 | 部分(Serial GC 不支持) | 部分并发 | 几乎全并发 |
| STW 时间 | 较长(Full GC 时) | 中等 | 极短(通常 < 1ms) |
3.2 各区域的作用
注意:本节仅适用于 Serial GC 和 Parallel GC 的分代布局。
| 区域 | 作用 | 特点 |
|---|---|---|
| Eden 区 | 大部分新对象在 Eden 区分配 | 生命周期短的对象较多 |
| Survivor 区 | 存放经过一次或多次 GC 仍然存活的对象 | 分为 From 和 To,空间互换 |
| 老年代 | 存放生命周期长的对象 | 存放经过多次 GC 仍然存活的对象 |
3.3 对象在堆中的生命周期
注意:本节描述的对象生命周期仅适用于 Serial GC 和 Parallel GC 等分代 GC。
对象创建
经历第一次 Minor GC
仍然存活
再次经历 Minor GC
仍然存活
年龄增加
年龄达到阈值
默认 15
对象不再被引用
被 Major GC 回收
Eden
Survivor
Old
新生代
生命周期短
GC 频繁
老年代
生命周期长
GC 不频繁
3.4 TLAB (Thread Local Allocation Buffer)
注意:TLAB 适用于有新生代概念的 GC(如 Serial、Parallel、G1、分代 ZGC)。
HotSpot 在新生代为每个线程分配了 TLAB,用于线程本地对象分配,避免加锁。
小对象分配
TLAB 满
TLAB 分配失败
线程
TLAB (Eden 区内)
Eden 区
公共区域
在 Eden 区
加锁分配
| 特性 | TLAB | Eden 公共区域 |
|---|---|---|
| 分配速度 | 极快(无锁) | 较慢(需要 CAS) |
| 空间浪费 | 可能有一定浪费 | 无浪费 |
| 适用场景 | 小对象 | 大对象或 TLAB 满时 |
3.5 堆外内存
HotSpot 还使用堆外内存存储特定数据:
堆外内存
DirectByteBuffer
代码缓存
线程栈
GC 数据结构
4. 栈
4.1 HotSpot 栈的统一实现
HotSpot 对虚拟机栈和本地方法栈使用同一套实现。
Java 栈
栈帧 1
methodA()
栈帧 3
methodC()
栈帧 2
methodB()
Java 线程
4.2 栈帧的内部结构
栈帧
局部变量表
操作数栈
动态连接
返回地址
Slot 0
Slot 1
...
Slot N
栈顶
...
栈底
4.3 虚拟线程的栈
JDK 21 引入的虚拟线程使用了不同的栈实现策略。
虚拟线程启动
IO 操作
持续等待
IO 完成
继续执行
栈帧在物理栈
CPU 寄存器
栈帧冻结到堆
链表结构
等待
栈帧拷贝回物理栈
CPU 寄存器
物理栈
高效执行
堆内存
轻量级
| 特性 | 平台线程 | 虚拟线程 |
|---|---|---|
| 栈类型 | 固定大小物理栈 | 连续内存块(堆) |
| 栈大小 | 约 1MB | 初始较小,按需扩展 |
| 上下文切换 | 昂贵(OS 级别) | 便宜(JVM 级别) |
| 数量 | 数千个 | 数百万个 |
4.4 逃逸分析与标量替换
HotSpot 的 JIT 编译器会进行逃逸分析,优化对象分配。
未逃逸 & 可拆解
未逃逸 但不可拆解
逃逸
对象创建
逃逸分析
标量替换
拆解为局部变量
(存储在栈/寄存器)
分配到堆
5. 程序计数器
5.1 HotSpot 程序计数器的实现
虚拟线程
运行时
CPU 寄存器
挂起时
堆内存
平台线程
PC 寄存器
CPU 硬件
5.2 不同状态下的 PC 寄存器
| 线程类型 | 执行状态 | PC 位置 |
|---|---|---|
| 平台线程 | 运行中 | CPU 寄存器 |
| 平台线程 | 阻塞 | CPU 寄存器(值不变) |
| 虚拟线程 | 运行中 | CPU 寄存器 |
| 虚拟线程 | 挂起 | 堆内存(作为上下文的一部分) |
| 执行 Native 方法 | - | Undefined(规范定义) |
6. 与其他 JVM 实现对比
6.1 主流 JVM 实现
主流实现
HotSpot
Oracle/OpenJDK
OpenJ9
Eclipse
Zing
Azul
GraalVM
Oracle
JVM 实现
6.2 各实现的内存管理对比
| 特性 | HotSpot | OpenJ9 | Zing | GraalVM(JVM模式) |
|---|---|---|---|---|
| 方法区实现 | 元空间 | 共享类缓存 | 共享类缓存 | 元空间(HotSpot 前端) |
| GC | G1, ZGC, Serial | GenCon, Balanced | C4 | G1, ZGC |
| JIT | C1/C2 分层编译 | J9 JIT | Falcon JIT | Graal JIT + Truffle |
| 内存占用 | 较高 | 较低 | 较低 | 较低 |
| 启动速度 | 中等 | 较快 | 快 | 慢(无预热) |
| 峰值性能 | 高 | 高 | 很高 | 很高 |
6.3 HotSpot 的优势
- 生态成熟:最广泛使用的 JVM 实现
- 性能优异:多年的优化积累
- GC 丰富:提供多种 GC 算法选择
- 工具完善:JFR、JCMD、JHSDB 等工具支持
- 社区活跃:OpenJDK 社区持续改进
6.4 HotSpot 的劣势
- 启动速度:相比 OpenJ9 和 Zing 较慢
- 内存占用:相比 OpenJ9 和 Zing 较高
- 预热时间:达到峰值性能需要预热
7. 总结
7.1 HotSpot 内存区域实现要点
- 元空间:使用本地内存,按类加载器隔离
- 堆:支持多种内存布局,取决于选择的 GC(分代、Region、分代 Region)
- 栈:统一实现,虚拟线程使用栈帧拷贝
- 程序计数器:利用 CPU 寄存器,虚拟线程支持挂起
7.2 HotSpot 的设计哲学
- 自适应优化:通过运行时分析优化性能
- 分层编译:平衡启动速度和峰值性能
- GC 为中心:以 GC 为核心设计内存布局