跳转至

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 能在目标时间内清完。

相关链接