适用读者:被"堆/栈/方法区/元空间"绕晕的人。
前置:S04-J03(字节码)。
实测环境:OpenJDK 21.0.12.1(GC 日志实测,2026-11-05)。
覆盖旧文:A-2575(1238) A-1644(393) A-1130(208) A-2334(363) + 1641(88) 2590(260) 1023(86) 1632(155) 1636(134) 1628(116) 1627(142) 1629(174) 1550(137) 1544(75) 1128(126) 1010(63) = 3,758 点。
覆盖扩展:E-S05-011(栈帧结构)、E-S05-012(可达性分析)。
上一篇:S05-J03 · 异步编排 CompletableFuture
下一篇:S05-J05 · 类加载与字节码执行。
1. 一句话结论
JVM 的内存分五块:方法区(元空间)、堆、虚拟机栈、本地方法栈、程序计数器。GC 只关心"堆"(以及元空间的类卸载)。
| 区域 | 线程共享 | 存什么 | 会 OOM 吗 |
|---|---|---|---|
| 堆 | ✅ | 对象实例 | ✅ OutOfMemoryError: Java heap space |
| 方法区/元空间 | ✅ | 类元数据、常量 | ✅ Metaspace |
| 虚拟机栈 | ❌ | 栈帧(局部变量、操作数栈) | ✅ StackOverflowError |
| 本地方法栈 | ❌ | native 方法 | ✅ |
| 程序计数器 | ❌ | 当前字节码行号 | ❌(唯一不会 OOM) |
2. 实测:GC 日志长什么样
$ java -Xmx128m -Xlog:gc:stdout G
实测输出(G1):
[0.015s][info][gc] Using G1
[0.629s][info][gc] GC(0) Pause Young (Concurrent Start) (G1 Humongous Allocation) 56M->55M(128M) 14.651ms
[0.646s][info][gc] GC(2) Pause Young (Concurrent Start) (G1 Humongous Allocation) 60M->60M(128M) 4.082ms
[0.656s][info][gc] GC(3) Pause Remark 65M->65M(128M) 1.514ms
[0.665s][info][gc] GC(3) Concurrent Mark Cycle 18.793ms
[0.786s][info][gc] GC(4) Pause Young (Prepare Mixed) (G1 Humongous Allocation) 125M->65M(128M) 3.998ms
[0.845s][info][gc] GC(5) Pause Young (Mixed) (G1 Humongous Allocation) (Evacuation Failure) 125M->70M(128M) 5.213ms
逐段读法:
| 片段 | 含义 |
|---|---|
Pause Young |
年轻代 GC(STW) |
56M->55M(128M) |
回收前→回收后(堆上限 128M) |
14.651ms |
停顿时间 |
Humongous Allocation |
分配了大对象(> Region 一半,8MB 数组就是) |
Concurrent Mark Cycle |
并发标记(不 STW) |
Pause Mixed |
混合回收(年轻代 + 部分老年代) |
Evacuation Failure |
晋升失败(目标区放不下)——这是要警惕的信号 |
Evacuation Failure 的教学价值:它说明"堆给小了 + 大对象多"会导致回收失败,进而 Full GC。调优的第一信号就是这个。
3. 对象分配与栈帧(E-S05-011)
| 概念 | 说明 |
|---|---|
| TLAB | 每个线程的私有分配缓冲区(减少竞争) |
| 对象头 | Mark Word(哈希/锁)+ 类型指针 |
| 栈帧 | 局部变量表 + 操作数栈 + 动态链接 + 返回地址 |
| 逃逸分析 | 对象不出方法 → 可能栈上分配/标量替换(不占堆) |
推论:"new 出来的对象一定在堆上"不完全对——JIT 可能优化掉。
4. 对象存活判定(E-S05-012)
| 方法 | 说明 |
|---|---|
| 引用计数 | 简单但有循环引用问题 → JVM 不用 |
| 可达性分析 | 从 GC Roots 出发不可达即可回收 |
GC Roots 包括:栈中局部变量、静态字段、常量、JNI 引用、活动线程。
四种引用:
| 引用 | 回收时机 |
|---|---|
| 强引用 | 永不(直到不可达) |
SoftReference |
内存不足时 |
WeakReference |
下次 GC(ThreadLocal 的 key 就是它) |
PhantomReference |
回收前通知(用于清理) |
5. 五个误区
| 误区 | 真相 |
|---|---|
| "程序计数器也会 OOM" | 唯一不会 |
| "对象一定分配在堆上" | 逃逸分析可能栈上分配 |
| "引用计数是 JVM 的 GC 算法" | JVM 用可达性分析 |
| "GC 日志只有停顿时间有用" | Evacuation Failure/晋升失败更关键 |
"-Xmx 越大越好" |
堆太大会拉长单次回收(G1 可分 Region 缓解) |
6. 自检
- 五个运行时区域里,哪些线程共享?哪个不会 OOM?
- GC 日志
56M->55M(128M) 14.651ms各段含义? Evacuation Failure说明什么?会导致什么?- 为什么 JVM 不用引用计数?
- 四种引用分别什么时候回收?
7. 下一篇
S05-J05 · 类加载与字节码执行
内存区讲完,下一篇讲"类怎么进来":双亲委派、三个阶段,以及为什么它会被"破坏"。
本篇属于系列《S05 · Java 并发与 JVM》。旧文来源:A-2575/1644/1130/2334/1641/2590/1023/1632/1636/1628/1627/1629/1550/1544/1128/1010;扩展点 E-S05-011/012。GC 日志为 OpenJDK 21 实测。