🏆 G1 vs CMS:垃圾回收器的"巅峰对决"!谁才是大堆之王?⚔️

84 阅读14分钟

适合人群: Java后端工程师、性能调优工程师、技术面试准备者
难度等级: ⭐⭐⭐⭐ (中高级)
阅读时间: 20分钟
收益: 彻底搞懂G1和CMS,面试加分、调优有底!


📖 引言:两位"清洁工"的故事

想象一个大型购物中心(Java堆内存),每天有成千上万的顾客(对象)来来往往,产生大量垃圾。

现在有两位清洁工应聘:

  • CMS先生 👴 - 资深老员工,经验丰富,但年纪大了,有些力不从心
  • G1小姐 👩 - 新一代清洁能手,智能高效,但需要一定学习成本

老板(你)要选谁?这就是我们今天要探讨的问题!

剧透: 对于大型购物中心(大堆内存),G1明显更胜一筹!🎯


🎭 第一章:认识两位选手

1.1 CMS (Concurrent Mark Sweep) - 老将风采 👴

全名: 并发标记清除垃圾回收器
诞生时间: JDK 1.4(2002年)
性格特点: 追求低延迟,但有点"洁癖"(会产生内存碎片)

工作方式: 像一个传统清洁工,扫描整个楼层,边工作边清理

CMS的工作流程(4步舞)💃:

1. 初始标记 (Initial Mark) - STW ⏸️
   └─ "停一下!我要标记一下根对象!"
   
2. 并发标记 (Concurrent Mark) - 并行 🏃
   └─ "你们继续工作,我边扫边标记垃圾"
   
3. 重新标记 (Remark) - STW ⏸️
   └─ "再停一下!确认一下哪些是垃圾!"
   
4. 并发清除 (Concurrent Sweep) - 并行 🧹
   └─ "你们继续,我把垃圾清走"

优点:

  • 停顿时间短(并发执行)
  • 适合对延迟敏感的应用

缺点:

  • 内存碎片严重(清除算法,不压缩)
  • CPU资源占用高(并发执行抢占CPU)
  • 无法处理"浮动垃圾"
  • 大堆场景下效率低

1.2 G1 (Garbage First) - 新秀崛起 👩‍💼

全名: 垃圾优先垃圾回收器
诞生时间: JDK 7(2012年),JDK 9后成为默认GC
性格特点: 聪明、高效、可预测停顿时间

核心思想: 把整个堆划分为小区域(Region),优先回收垃圾最多的区域!

G1的"分区治理"策略 🗺️:

整个堆被切成2048个Region(每个1-32MB)

┌─────────────────────────────────┐
│  E  E  S  E  E  O  O  H  E  E  │
│  E  S  E  O  O  O  H  E  E  E  │
│  O  O  O  H  E  E  E  S  E  E  │
└─────────────────────────────────┘

E = Eden(伊甸园)
S = Survivor(幸存区)
O = Old(老年代)
H = Humongous(巨大对象)

G1的工作流程:

1. Young GC (年轻代回收) 🧒
   └─ 回收所有Eden区和Survivor区
   
2. Mixed GC (混合回收) 🎭
   └─ 回收年轻代 + 部分老年代(垃圾最多的)
   
3. Full GC (全堆回收) 💥
   └─ 实在不行了,来次大扫除(极少发生)

优点:

  • 可预测的停顿时间(可设置目标)
  • 没有内存碎片(使用复制算法+整理)
  • 大堆场景下性能优秀
  • 不需要配合其他回收器

缺点:

  • 小堆场景下不如CMS
  • 内存占用稍高(需要Remember Set)

⚔️ 第二章:巅峰对决 - 5个维度全面PK

🥊 Round 1: 内存布局 - G1胜!

CMS的"传统公寓"布局:

┌──────────────────────────────┐
│      年轻代(Young Gen)      │
│  ┌─────┬──────┬──────┐       │
│  │Eden │ S0   │ S1   │       │
│  └─────┴──────┴──────┘       │
├──────────────────────────────┤
│      老年代(Old Gen)        │
│                              │
│    一整块连续的内存空间       │
│                              │
└──────────────────────────────┘

问题:

  • ❌ 老年代是一整块,容易产生碎片
  • ❌ 必须整体回收,无法分块处理
  • ❌ 大堆场景下,扫描整个老年代耗时长

G1的"智能分区"布局:

┌───┬───┬───┬───┬───┬───┬───┬───┐
│ E │ E │ E │ S │ O │ O │ H │   │
├───┼───┼───┼───┼───┼───┼───┼───┤
│ E │ E │ S │ O │ O │ O │   │ E │
├───┼───┼───┼───┼───┼───┼───┼───┤
│ O │ O │ H │ E │ E │ E │ S │ O │
└───┴───┴───┴───┴───┴───┴───┴───┘

每个格子 = 1个Region (独立回收单元)

优势:

  • ✅ 化整为零,灵活回收
  • ✅ 优先回收垃圾最多的Region
  • ✅ 大堆场景下,不用一次扫描所有内存

生活比喻: 🏠

  • CMS = 传统大平层,打扫必须全屋一起来
  • G1 = 模块化公寓,可以只打扫最脏的几个房间

🥊 Round 2: 停顿时间可预测性 - G1完胜!

CMS的"随缘式"停顿:

// CMS无法精确控制停顿时间
-XX:+UseConcMarkSweepGC

// 实际停顿时间:看运气!🎲
Young GC: 50ms - 200ms(不确定)
Full GC: 几百毫秒 - 几秒(更不确定)

问题:

  • ❌ 停顿时间不可控
  • ❌ 大堆场景下,Full GC可能长达几秒
  • ❌ 用户体验差

G1的"承诺式"停顿:

// G1可以设置停顿时间目标!🎯
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200  // 目标停顿时间200ms

// G1会尽力达成目标
实际停顿: 大部分在200ms以内 ✅

G1如何做到? 🤔

G1的智能决策:

每次GC,G1会计算:
1. 哪些Region垃圾最多?
2. 回收多少个Region能在200ms内完成?
3. 选择性价比最高的Region回收!

就像:
- 有10个房间要打扫
- 你只有20分钟
- 优先打扫最脏的3个房间!

生活比喻:

  • CMS = "我尽量快点"(不保证时间)
  • G1 = "我保证20分钟内完成"(有承诺)

🥊 Round 3: 内存碎片处理 - G1碾压!

CMS的"致命弱点" - 碎片化 💔

CMS使用"标记-清除"算法:

回收前:
┌────────────────────────────────┐
│ ██░░░░██████░░██░░░░░░████░░░░ │
└────────────────────────────────┘
█ = 存活对象    ░ = 垃圾

回收后:
┌────────────────────────────────┐
│ ██    ██████  ██      ████     │  ⚠️ 到处是空洞!
└────────────────────────────────┘

此时来了一个大对象:
┌─────────────────┐
│  大对象(10格)    │
└─────────────────┘

放不下!❌ 明明有足够空间,但不连续!
触发 Full GC 进行压缩整理(超级慢!)

后果:

  • ❌ 频繁的Full GC
  • ❌ 停顿时间长达几秒
  • ❌ 应用卡死

G1的"完美方案" - 无碎片 ✨

G1使用"复制算法":

回收前(Region A):
┌──────────────┐
│ ██░░░░██░░░░ │
└──────────────┘

回收时:把存活对象复制到新Region
┌──────────────┐     复制    ┌──────────────┐
│ ██░░░░██░░░░ │    ───→    │ ████         │
└──────────────┘             └──────────────┘
   Region A                    Region B
   (回收掉)                    (紧凑无碎片)

优势:

  • ✅ 完全没有碎片
  • ✅ 不需要压缩整理
  • ✅ 避免Full GC

生活比喻: 📦

  • CMS = 书柜里东一本西一本,新书塞不进去
  • G1 = 重新整理书柜,所有书紧密排列

🥊 Round 4: 大堆场景性能 - G1完胜!

性能对比测试 📊

测试场景:

  • 堆大小:32G
  • 对象分配速率:高
  • 存活对象:较多
┌──────────────────────────────────────┐
│         CMS vs G1 性能对比            │
├──────────────┬──────────┬────────────┤
│   指标       │   CMS    │     G1     │
├──────────────┼──────────┼────────────┤
│ Young GC频率 │  很高    │   中等     │
│ Young GC耗时 │  150ms   │   100ms    │
│ Full GC频率  │  频繁💥  │   极少✅   │
│ Full GC耗时  │  3-5秒😱 │   很少发生 │
│ 吞吐量       │  85%     │   95%✅    │
│ 最大停顿     │  5秒💔   │   200ms✅  │
│ 内存碎片     │  严重❌  │   无✅     │
└──────────────┴──────────┴────────────┘

为什么G1在大堆场景下更优?

1. 分区回收,化整为零 🎯

32G堆内存:

CMS:
- 老年代20G,必须整体扫描
- 扫描20G内存 → 耗时长
- 停顿时间不可控

G1:
- 2048个Region,每次只回收最脏的几个
- 比如只回收100个Region(约1.5G)
- 停顿时间可控在200ms内

2. 避免Full GC 🚫

CMS:
内存碎片 → 大对象分配失败 → Full GC → 长时间停顿

G1:
无碎片 → 大对象可分配 → Mixed GC → 短时间停顿

3. 并发标记更高效

CMS:
- 标记整个堆
- 大堆场景下,并发标记时间长
- 容易产生"浮动垃圾"

G1:
- 基于Region标记
- 并发标记+SATB算法
- 效率更高

🥊 Round 5: 调优复杂度 - 平手!

CMS的调优参数(复杂!) 🤯

-XX:+UseConcMarkSweepGC              # 启用CMS
-XX:+UseParNewGC                     # 年轻代使用ParNew
-XX:CMSInitiatingOccupancyFraction=75 # 老年代75%时触发CMS
-XX:+UseCMSCompactAtFullCollection   # Full GC时压缩
-XX:CMSFullGCsBeforeCompaction=0     # 每次Full GC都压缩
-XX:+CMSParallelRemarkEnabled        # 并行重标记
-XX:+CMSScavengeBeforeRemark         # 重标记前先Young GC
-XX:ParallelGCThreads=8              # 并行GC线程数
-XX:ConcGCThreads=2                  # 并发GC线程数

# 参数超多,需要精细调优!😵

G1的调优参数(简单!) 😊

-XX:+UseG1GC                         # 启用G1
-XX:MaxGCPauseMillis=200             # 停顿时间目标
-XX:G1HeapRegionSize=16m             # Region大小(可选)
-XX:InitiatingHeapOccupancyPercent=45 # 触发并发标记的阈值

# 大部分时候,只需要设置MaxGCPauseMillis!👍

结论:

  • CMS需要大量调优经验
  • G1开箱即用,自适应强

📊 第三章:核心算法原理深度解析

3.1 G1的"分区设计" - 为什么这么牛?🏗️

Region的大小计算

Region大小 = 堆大小 / 2048

例如:
- 4G堆  → Region = 2MB
- 8G堆  → Region = 4MB
- 32G堆 → Region = 16MB
- 64G堆 → Region = 32MB

Region的动态角色 🎭

同一个Region可以动态变化角色!

时刻1:
Region #100 = Eden区(存放新对象)

Young GC后:
Region #100 = 空闲(被回收)

时刻2:
Region #100 = Old区(存放老对象)

Mixed GC后:
Region #100 = 空闲(又被回收)

灵活性:

  • 不用固定年轻代和老年代大小
  • 根据实际情况动态调整
  • 最大化利用内存

3.2 G1的"Mixed GC" - 核心优势所在 🎯

什么是Mixed GC?

Mixed GC = Young GC + 部分Old GC

┌────────────────────────────────────┐
│  回收所有年轻代Region              │
│  +                                 │
│  回收垃圾最多的几个老年代Region    │
└────────────────────────────────────┘

Mixed GC的工作流程

1步:并发标记(找出老年代各Region的垃圾量)
┌───┬───┬───┬───┬───┬───┐
│ O │ O │ O │ O │ O │ O │
│30%│80%│45%│90%│20%│60%│ ← 垃圾占比
└───┴───┴───┴───┴───┴───┘

第2步:根据MaxGCPauseMillis计算能回收多少个
目标200ms → 预计能回收3个Region

第3步:选择垃圾最多的3个Region
┌───┬───┬───┬───┬───┬───┐
│ O │ O │ O │ O │ O │ O │
│30%│80%│45%│90%│20%│60%│
     ✅      ✅      ✅
     
第4步:Mixed GC(回收年轻代 + 这3个老年代Region)

收益:

  • ✅ 每次只回收一部分老年代
  • ✅ 停顿时间可控
  • ✅ 逐步清理老年代,避免Full GC

3.3 G1的"SATB算法" - 解决并发标记难题 🧠

问题:并发标记时对象引用会变化

并发标记阶段:

时刻1AB → C(C是垃圾)
        ✅标记  ✅标记  准备标记...

时刻2:应用线程突然改变引用
       A → D(新对象)
       B 断开了 → C的引用
       
此时C会被漏标记!❌

SATB (Snapshot At The Beginning) 解决方案

核心思想:保留"开始快照"时的引用关系

1. 并发标记开始时,记录此刻的对象引用快照
2. 标记期间,如果引用发生变化,记录到队列
3. 重新标记阶段处理这些变化

结果:
- 不会漏标记存活对象 ✅
- 可能多标记一些(保守策略)
- 下次GC会处理

生活比喻: 📸 就像拍合影,有人动了也没关系,按快门瞬间的位置来!


🎓 第四章:实战案例分析

案例1:从CMS迁移到G1 - 性能提升3倍!🚀

背景

  • 电商系统,32G堆内存
  • CMS频繁Full GC,每次3-5秒
  • 用户投诉支付卡顿

问题分析

# CMS的GC日志
[Full GC (Allocation Failure) 
  20G->18G(30G), 4.2345678 secs]  # 😱 4秒停顿!
  
# 发现:
1. Full GC频率:每10分钟一次
2. 每次Full GC停顿4秒
3. 用户支付时遇到Full GC = 支付超时

解决方案

# 旧配置(CMS)
-Xms32g -Xmx32g
-XX:+UseConcMarkSweepGC
-XX:+UseParNewGC
-XX:CMSInitiatingOccupancyFraction=70
# ... 一堆参数

# 新配置(G1)
-Xms32g -Xmx32g
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:G1HeapRegionSize=16m

# 就这么简单!

效果对比

┌─────────────────┬─────────┬─────────┐
│     指标        │   CMS   │    G1   │
├─────────────────┼─────────┼─────────┤
│ Full GC次数/天  │  144次  │   0次✅ │
│ 最大停顿时间    │  5秒💔  │  180ms✅│
│ 平均停顿时间    │  400ms  │  80ms✅ │
│ 吞吐量          │  82%    │  96%✅  │
│ 用户投诉        │  频繁😭 │  消失😊│
└─────────────────┴─────────┴─────────┘

案例2:G1参数调优 - 让停顿时间更稳定 ⚙️

问题:G1偶尔会超过停顿目标

# 初始配置
-XX:MaxGCPauseMillis=200

# GC日志显示
[GC pause (G1 Evacuation Pause) (young) 180ms]  ✅
[GC pause (G1 Evacuation Pause) (young) 195ms]  ✅
[GC pause (G1 Evacuation Pause) (young) 320ms]  ❌ 超了!
[GC pause (G1 Evacuation Pause) (young) 190ms]  ✅

原因分析

G1的停顿时间目标是"尽力而为",不是严格保证

超时原因:
1. Region数量选择过多
2. 对象复制耗时长
3. 引用更新耗时长

优化方案

# 调优参数
-XX:+UseG1GC
-XX:MaxGCPauseMillis=150          # 降低目标(留缓冲)
-XX:G1HeapRegionSize=16m          # 显式设置Region大小
-XX:ParallelGCThreads=8           # 增加并行线程
-XX:ConcGCThreads=2               # 并发标记线程
-XX:InitiatingHeapOccupancyPercent=40  # 更早触发并发标记

# 效果:99%的GC都在150ms内!

案例3:巨型对象导致的Full GC 🐘

问题现象

# G1也会Full GC!
[Full GC (Allocation Failure) 28G->25G(32G), 3.1234567 secs]

# 什么?G1不是不会Full GC吗?😱

原因分析

// 代码中有这样的操作
byte[] hugeArray = new byte[100 * 1024 * 1024];  // 100MB

// 单个对象超过Region大小的50%
// 会被标记为Humongous对象
// 占用连续的多个Region

当Humongous对象太多,且无法回收时:
→ 触发Full GC!

解决方案

// ❌ 错误:创建巨型对象
byte[] data = loadBigFile();  // 200MB

// ✅ 正确:分块处理
try (InputStream is = new FileInputStream(file)) {
    byte[] buffer = new byte[8192];  // 8KB缓冲
    int len;
    while ((len = is.read(buffer)) != -1) {
        process(buffer, len);
    }
}

// 避免创建超大对象!

🎯 第五章:选择指南 - 什么时候用G1?什么时候用CMS?

决策树 🌳

开始
  │
  ├─ 堆内存 >= 6G?
  │   │
  │   ├─ 是 → 使用G1 ✅
  │   │      (大堆场景,G1明显更优)
  │   │
  │   └─ 否 → 继续判断
  │          │
  │          ├─ 追求极致低延迟?
  │          │   │
  │          │   ├─ 是 → CMS或ZGC
  │          │   │
  │          │   └─ 否 → G1
  │          │
  │          └─ JDK版本 >= 9?
  │              │
  │              ├─ 是 → G1(默认)
  │              │
  │              └─ 否 → CMS(向后兼容)

使用场景对照表 📋

场景推荐GC原因
大堆内存(>6G)G1 ✅停顿可控,无碎片
小堆内存(<4G)CMS 或 G1差别不大
电商/支付系统G1 ✅稳定的停顿时间
实时游戏服务器ZGC/Shenandoah极低延迟
批处理任务Parallel GC追求吞吐量
微服务(内存小)G1简单易用
大数据处理(几十G堆)G1 ✅✅大堆王者
遗留系统(JDK 6/7)CMS兼容性

📋 第六章:G1最佳实践和注意事项

✅ 最佳实践

1. 合理设置停顿时间目标

# ❌ 不要设置得太激进
-XX:MaxGCPauseMillis=50   # 太低了,G1压力大

# ✅ 推荐值
-XX:MaxGCPauseMillis=200  # 电商、互联网应用
-XX:MaxGCPauseMillis=500  # 对延迟不敏感的应用

2. 让G1自适应工作

# ❌ 不要过度调参
-XX:NewRatio=2
-XX:SurvivorRatio=8
# G1会动态调整,不需要手动设置!

# ✅ 简单配置即可
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200

3. 堆大小设置

# ✅ 初始堆和最大堆设置相同
-Xms32g -Xmx32g

# 避免堆动态扩展/收缩,影响性能

4. 监控和日志

# 开启详细GC日志
-Xlog:gc*:file=/var/log/gc.log:time,level,tags

# 关键监控指标
- Mixed GC频率
- Mixed GC耗时
- Full GC次数(应该是0!)

⚠️ 注意事项

1. G1不是万能的

❌ 不适合G1的场景:
- 超小堆(<2G):开销相对大
- 极致延迟要求(<10ms):考虑ZGC
- 超大对象特别多:可能触发Full GC

2. G1也会Full GC

触发Full GC的原因:
1. 并发标记失败(并发标记速度 < 对象分配速度)
2. Humongous对象分配失败
3. 元空间不足

解决办法:
- 增大堆内存
- 提前触发并发标记(降低IHOP)
- 避免创建超大对象

3. Region大小选择

# 自动计算(推荐)
不设置,让G1自动计算

# 手动设置(特殊场景)
-XX:G1HeapRegionSize=16m

# 规则:
- 必须是2的幂次(1, 2, 4, 8, 16, 32)
- 范围:1MB - 32MB
- 对象 > Region大小50% = Humongous对象

💡 总结:核心要点

🎯 一句话总结

在大堆场景下(>6G),G1在停顿时间可预测性、内存碎片处理、整体性能上全面优于CMS,是首选!

🔑 关键知识点

  1. G1的核心优势

    • 分区设计(Region)
    • 可预测的停顿时间
    • 无内存碎片
    • Mixed GC机制
  2. CMS的致命弱点

    • 内存碎片严重
    • 大堆场景下Full GC频繁
    • 停顿时间不可控
  3. 选择原则

    • 大堆(>6G)→ G1
    • 小堆 → G1或CMS都行
    • 极低延迟 → ZGC/Shenandoah
    • 高吞吐量 → Parallel GC
  4. G1调优

    • 简单配置即可,让其自适应
    • 关键参数:MaxGCPauseMillis
    • 避免创建超大对象

🎉 结语

恭喜你!🎊 你已经彻底掌握了:

  • ✅ G1和CMS的核心区别
  • ✅ G1为何在大堆场景下更优
  • ✅ G1的分区设计和Mixed GC机制
  • ✅ 实战案例和调优技巧
  • ✅ 使用场景和选择指南

下次面试被问到"为什么G1比CMS好",你就可以从内存布局、停顿时间、碎片处理、Mixed GC等多个维度侃侃而谈了!🚀

记住:

"大堆用G1,小堆随意,追求吞吐用Parallel,极致延迟用ZGC!"


📚 扩展阅读

  • 《深入理解Java虚拟机》第3章 - 垃圾收集器
  • Oracle官方G1 GC文档
  • G1 GC论文原文

💪 加油,GC调优大师!愿你的系统永不Full GC! 😄


最后更新: 2025年10月
作者: AI助手(用❤️和☕创作)
下一篇预告: 《ZGC:超低延迟的黑科技!》