GC 回收器对比¶
从 Serial 到 ZGC,各回收器的核心差异在于吞吐量与延迟的取舍;G1 取代 CMS 成为主流,因为解决了碎片和可预测暂停问题。
一句话比喻¶
把 Java 堆想象成一栋写字楼:
🏢 Serial GC = 一个保安自己扫
🏢 Parallel GC = 一群保安一起扫
🏢 CMS GC = 保安边上班边偷偷扫
🏢 G1 GC = 按楼层分区,哪脏扫哪
🏢 ZGC GC = 超级保安,几乎感觉不到
各回收器定位¶
| 回收器 | JDK 版本 | 作用范围 | 核心特点 |
|---|---|---|---|
| Serial | 1.0+ | 全堆 | 单线程,STW,最简单 |
| Parallel | 1.2+ | 全堆 | 多线程 STW,追求吞吐量 |
| ParNew | 1.4+ | 年轻代 | 多线程版 Serial,配合 CMS 用 |
| CMS | 1.5+ | 老年代 | 尽量并发,追求低延迟 |
| G1 | 1.7+ | 全堆 | 分 Region,可预测暂停 |
| ZGC | 15+ | 全堆 | 暂停 <1ms,超大堆 |
| Shenandoah | 12+ | 全堆 | 并发压缩,低延迟 |
CMS 回收器(Concurrent Mark Sweep)¶
核心思想:老年代的垃圾,尽量让 GC 线程和业务线程同时跑。
四个阶段¶
sequenceDiagram
participant B as 业务线程
participant G as GC 线程
Note over B: 正常运行
B->>B: 正常运行
Note over G: ① 初始标记(极短 STW)
G->>G: 标记 GC Roots 直接引用
Note over B,G: ② 并发标记(不暂停业务)
B->>B: 正常运行
G->>G: 遍历对象图(耗时最长)
Note over G: ③ 重新标记(短 STW)
G->>G: 修正并发阶段变动
Note over B,G: ④ 并发清除(不暂停业务)
B->>B: 正常运行
G->>G: 清除标记为垃圾的对象
每个阶段详解¶
① 初始标记(Initial Mark)⏱ 极短,STW¶
只标记 GC Roots 直接引用的对象,不做全图遍历,所以很快。
② 并发标记(Concurrent Mark)⏱ 最长,但不暂停业务¶
从 GC Root 出发,遍历整个对象图。GC 线程和业务线程同时运行。⚠️ 这阶段业务可能产生新的垃圾,标记不到。
③ 重新标记(Remark)⏱ 短,STW¶
修正并发阶段产生的变动,用了 Incremental Update 算法。
④ 并发清除(Concurrent Sweep)⏱ 长,但不暂停业务¶
清除标记为垃圾的对象,GC 线程和业务线程同时运行。
CMS 的四个致命问题¶
问题 1:浮动垃圾(Floating Garbage)¶
并发阶段产生的新垃圾,重新标记阶段才发现不了,只能等下次 GC 才能回收。需要保守预留更多老年代空间。
问题 2:内存碎片¶
CMS 用的是标记-清除,不压缩。久了之后老年代全是碎片:
想分配一个大对象?放不下 → 触发 Full GC(退化为 Serial 的标记-压缩)。这是最致命的。
问题 3:Concurrent Mode Failure¶
GC 还没清完,老年代就满了 → 被迫 STW 做一次 Full GC → 延迟暴增,CMS 的初衷白费了。
问题 4:CPU 敏感¶
并发阶段抢占 CPU 资源。默认 GC 线程数 = (CPU 核数 + 3) / 4。4 核机器上 GC 吃掉 25% CPU。
G1 回收器(Garbage-First)¶
核心思想:把堆切成很多小 Region,哪里垃圾最多先扫哪里。
堆的结构¶
传统 GC 把堆分成连续的年轻代/老年代,G1 则把堆切成 Region:
graph TD
subgraph 传统布局
A[Young Gen] --- B[Old Gen]
end
subgraph G1 布局
C[Region 1: E] --- D[Region 2: E] --- E[Region 3: S] --- F[Region 4: O] --- G[Region 5: H] --- H[Region 6: free]
end
G1 布局示例(每个 Region 1~32MB,堆被切成约 2048 个):
┌────┬────┬────┬────┬────┬────┬────┬────┐
│ E │ E │ E │ S │ O │ O │ H │free│
├────┼────┼────┼────┼────┼────┼────┼────┤
│ O │free│ E │ E │ O │free│ O │ O │
├────┼────┼────┼────┼────┼────┼────┼────┤
│free│ E │ O │free│free│ S │ E │free│
└────┴────┴────┴────┴────┴────┴────┴────┘
E = Eden S = Survivor O = Old H = Humongous(大对象) free = 空闲
G1 的回收策略¶
"Garbage-First" = 优先回收垃圾最多的 Region。
每个 Region 的角色是**动态分配**的:
Region #5 角色变化:
│
├─→ 这轮是 Eden ← 新对象分配在这里
├─→ 下轮变成 free ← GC 后清空
├─→ 再下轮变成 Old ← 有对象晋升过来
└─→ 之后变成 free ← 老年代 GC 后释放
G1 的核心优势:可预测暂停¶
调优命令:-XX:MaxGCPauseTime=200ms
G1 自动计算:"我有 200ms 时间,哪些 Region 垃圾最多又能在 200ms 内清完?"
→ 挑一组 Region 收割
→ 保证暂停在 200ms 以内
时间轴:
══╪════════╪═══════╪═══════╪═════════
180ms 150ms 160ms 170ms ← 每次都在目标内
G1 的工作阶段¶
| 阶段 | 描述 | STW |
|---|---|---|
| 年轻代 GC | Eden 满 → 存活对象复制到 Survivor Region | ✅ 短 |
| 初始标记 | 标记 GC Roots(搭年轻代 GC 的便车) | ✅ 短 |
| 并发标记 | 和业务线程并行遍历对象图 | ❌ 不暂停 |
| 重新标记 | 用 SATB 修正并发阶段变动 | ✅ 短 |
| 清除 | 清理空 Region,为 Mixed GC 做准备 | ✅ 短 |
| Mixed GC | 回收年轻代 + 选一批垃圾多的老年代 Region | ✅ 可控 |
G1 动态调整代大小¶
默认:
-XX:G1NewSizePercent=5 年轻代最小 5%
-XX:G1MaxNewSizePercent=60 年轻代最大 60%
场景 1:大量短命对象 → G1 自动扩大年轻代
┌──────────────────────────────────┐
│ E E E E E E E E E E E │ O O O O │
│ 年轻代 (80%) │老年代20% │
└──────────────────────────────────┘
场景 2:大量长命对象 → G1 自动缩小年轻代
┌──────────────┬──────────────────────────┐
│ E E E │ O O O O O O O O O O O O O O O O│
│ 10% │ 老年代 90% │
└───────┴────────────────────────────────┘
CMS vs G1 正面对比¶
| 维度 | CMS | G1 |
|---|---|---|
| 作用范围 | 仅老年代 | 全堆(年轻代 + 老年代) |
| 算法 | 标记-清除(不压缩) | 标记-复制 + 标记-压缩 |
| 内存碎片 | ❌ 有,严重 | ✅ 无(Region 间整理) |
| 暂停时间 | 不可控 | 可预测(设目标值) |
| 浮动垃圾 | 有,需预留空间 | 有,但 Mixed GC 兜底 |
| 大堆表现 | 差(碎片 + 并发慢) | 好(Region 分治) |
| 小堆表现 | 还行 | 开销略高(Region 管理) |
| 并发阶段 | 并发标记 + 并发清除 | 并发标记(清除是 STW) |
| JDK 默认 | JDK 8 可选 | JDK 9+ 默认 |
为什么 CMS 被 G1 取代¶
CMS 的致命伤:
1. 碎片 → 大对象分配失败 → Full GC → 延迟暴增
2. Concurrent Mode Failure → Full GC → 延迟暴增
3. 不压缩 → 这两个问题无解
4. JDK 9 开始标记为废弃
G1 的解决方式:
1. Region 本身就是压缩的 → 无碎片
2. Mixed GC 主动回收老年代 → 不会等到老年代爆满
3. 可控暂停 → 不会出现意外的长暂停
4. JDK 9+ 默认 GC,生产环境广泛验证
练习题¶
题目一:CMS 的"标记-清除"有什么缺点?为什么 G1 要改成"标记-压缩"?
答案
标记-清除不移动对象,释放后留下碎片。长时间运行后老年代全是碎片,大对象分配失败,被迫触发 Full GC。G1 在 Region 级别做标记-压缩,存活对象复制到新 Region,旧 Region 直接回收,天然无碎片。
题目二:G1 的"可预测暂停"是怎么实现的?
答案
G1 在并发标记结束后,会计算每个 Region 的垃圾比例和回收收益。然后根据用户设定的目标暂停时间(-XX:MaxGCPauseTime),贪心地选择一组 Region 进行 Mixed GC,确保选中的 Region 能在目标时间内清完。