Skip to content

垃圾回收算法与收集器 ​

S6-009建议学高级约 90 分钟JDK 21Java 高级与架构 · JVM

1. 本节定位 ​

同一程序(100 轮 x 1MB 短命对象)、128M 堆、三种收集器的一手对比(-Xlog:gc 采集):

text
SerialGC    :Pause 5 次,例 18M->2M 0.591ms   ← 单次停顿最短 ✓✓
ParallelGC  :Pause 4 次,例 33M->1M 1.346ms
G1GC        :Pause 3 次,例 38M->1M 1.590ms   ← 次数最少 ✓
全部 0 次 Full GC ✓

★★ 一个反直觉的实测结论:小堆(128M)下 G1 并不占优,Serial 单次停顿最短 ✓✓ —— G1 官方建议用于 6G+ 堆 ✓ ——收集器选型必须结合堆大小 ✓✓✓

2. 学习目标 ​

  • 说清三种基础算法的取舍 ★★★;
  • 理解分代假说与「为什么这样分代」(有 《JVM 内存结构与对象创建》 实测证据)★★★;
  • 会按堆大小与延迟要求选收集器 ★★★;
  • 能读 -Xlog:gc 日志的「回收量/停顿」✓✓。

3. 前置知识检查 ​

4. 为什么需要这个知识 ​

★ GC 停顿 = 全世界暂停 ✗✗(除 ZGC 亚毫秒外)—— P99 毛刺的第一嫌疑人就是 GC ✗✓(呼应 T2-014)

5. 核心概念 ​

5.1 三种基础算法 ★★★ ​

算法做法优点缺点
标记-清除标记存活,清除垃圾简单内存碎片 ✗✗
标记-复制存活对象复制到另一半无碎片、快 ✓✓浪费一半空间 ✗
标记-整理标记后向一端移动无碎片、不浪费移动对象成本高 ⚠️

★ 分代的由来 ✓✓✓:绝大多数对象朝生夕死(《JVM 内存结构与对象创建》 实测:100MB 短命对象一次 Young GC 就清完 ✓)—— 新生代用复制(存活少 → 复制成本低 ✓✓);老年代用标记-整理/清除 ✓✓✓

5.2 收集器谱系(JDK 21)★★★ ​

收集器算法线程适用
Serial复制 + 整理单线程小堆/单核/客户端 ✓✓
Parallel复制 + 整理多线程(吞吐优先)批处理/计算 ✓
G1(默认)Region 复制+整理并发+并行大堆(6G+)低延迟 ✓✓
ZGC着色指针并发并发超大堆、亚毫秒停顿 ✓✓✓
Shenandoah并发整理并发同 ZGC ✓

★ JDK 21 的默认就是 G1(本节实测 "Using G1" ✓);CMS 已移除 ✗

5.3 选型决策(实测背书)★★★ ​

text
堆 < 1G 或单核            → Serial(实测单次停顿 0.59ms 最短)✓✓
吞吐优先(批处理)        → Parallel(吞吐 = 总停顿最短)✓
大堆 + 低延迟(在线服务)→ G1(6G+)✓✓
超大堆 + 极致延迟        → ZGC(<1ms,牺牲吞吐 10~15%)⚠️✓

★ 本节实测的选型印证 ✓✓✓:

128M 小堆:Serial 每次回收 18M->2M 用 0.59ms —— G1 的 Region 多、根扫描成本在小堆上不划算 ✓✓ —— 「没有最好的收集器,只有匹配堆大小与延迟目标的收集器」 ✓✓✓

5.4 读 GC 日志的三个数 ★★★ ​

text
GC(3) Pause Young (Allocation Failure) 33M->1M(83M) 1.346ms
       ↑原因            ↑回收前→回收后(堆容量) ↑停顿

★ 健康判据 ✓✓:

信号判断
Young 频繁但每次 <10ms正常 ✓
老年代增长快 → 频繁 Mixed/Full ✗✗泄漏或缓存过大 ⚠️
Full GC 出现 ✗✗✗立即排查(Meta/Humongous/碎片)✓✓
Pause 随时间变长 ✗碎片化 ✗

6. 递进示例 ​

6.1 对比实验(本节实测) ​

powershell
foreach ($g in @("SerialGC","ParallelGC","G1GC")) {
  java "-XX:+Use$g" "-Xlog:gc" "-Xmx128m" "-Xms64m" -cp java-deep/target/classes cn.kb.learn.deep.jvm.GcLesson
}

6.2 反例:小堆硬上 ZGC ​

text
ZGC 自身需要额外内存与并发线程 —— 128M 堆上得不偿失 ✗✗

7. 完整可运行代码与实测 ​

代码:labs/java-deep/src/main/java/cn/kb/learn/deep/jvm/GcLesson.java

实测输出(关键部分):

text
SerialGC   : Pause=5  例 18M->2M(61M) 0.591ms
ParallelGC : Pause=4  例 33M->1M(83M) 1.346ms
G1GC       : Pause=3  例 38M->1M(66M) 1.590ms
全部 Full=0;三方案剩余库存/结果一致 ✓

★ 三个数字读法 ✓✓:33M->1M = 回收前活 33M、回收后活 1M(32M 是垃圾被回收 ✓)、堆容量 83M ✓

8. Debug 观察点 ​

  1. 三种收集器各跑一次(本节已做)✓✓✓;
  2. 对比 ZGC:java "-XX:+UseZGC" "-Xlog:gc" ... 观察亚毫秒停顿 ✓✓;
  3. 把 -Xmx 调到 4G 再跑 G1:观察 Region 与次数变化 ✓。

9. 常见错误 ​

#错误说明
1小堆用 G1 ⚠️实测不占优 ✓✓
2误把 "Allocation Failure" 当错误 ✗✗这是 Young GC 的正常原因 ✓✓
3忽略 Full GC ✗✗✗它是最危险的信号 ✓✓
4ZGC 当默认 ✗吞吐损失 10~15% ⚠️
5只调收集器不查泄漏 ✗✗泄漏时换任何收集器都白搭 ✓✓

10. 动手实验 ​

  1. 三收集器对比(本节已做)✓✓✓;
  2. 加 ZGC 对比停顿 ✓✓;
  3. 故意泄漏(List 不断 add)观察 Old 增长与 Full GC ✗✗✓✓;
  4. 调 -Xmx 观察 GC 次数与停顿的变化 ✓。

验收方式:给一份 GC 日志能判断收集器、代行为与健康状态。

扩展任务:写一份「本服务 GC 基线」(收集器/堆参数/正常 GC 频率与停顿),供告警阈值参考 ✓✓

11. 练习题 ​

  1. 三种基础算法各自的优缺点?
  2. 为什么新生代用复制算法?
  3. Serial/Parallel/G1 的单次停顿实测排序(小堆)?
  4. "Allocation Failure" 是错误吗?
  5. 什么信号必须立即排查?
参考答案(要点)
  1. 清除:简单但碎片 ✗✗;复制:无碎片快但费空间 ✗;整理:无碎片不浪费但移动成本高 ⚠️✓
  2. 存活对象少 → 复制成本低 ✓✓(分代假说,《JVM 内存结构与对象创建》 实测)★
  3. Serial 0.59ms < Parallel 1.35ms < G1 1.59ms(本机 128M 实测)✓✓ —— 但 G1 次数最少,总停顿要看次数 x 单次 ✓★
  4. 不是 ✓✓ ——Eden 满了申请分配失败 = 正常的 Minor GC 触发原因 ✓✓★
  5. Full GC ✗✗✓(以及老年代持续增长、停顿随时间变长)✓✓★

12. 掌握标准 ​

  • [ ] 说清三种算法取舍 ★★★;
  • [ ] 理解分代假说与实测证据 ★★★;
  • [ ] 会按堆大小选收集器 ★★★;
  • [ ] 能读 -Xlog:gc 三元组 ★★★;
  • [ ] 知道 Full GC 的严重性 ★★★。

13. 扩展资源 ​

14. 下一步学习 ​

《GC 日志分析与 JVM 调优》。

进度只保存在本机浏览器,可随时清除

个人学习项目:AI 生成内容均经人工复核,第三方资源仅作索引