Java中垃圾回收器 G1 和 CMS 有什么区别?

它们各自想解决什么

前面几款收集器(Serial、ParNew、Parallel Scavenge)的思路都是分代 + 整块停顿:回收的时候把用户线程全部停下来,收完再继续。区别主要在于能不能并行、用的是复制还是标记-整理。

问题在于堆越来越大,一次全停顿的时间越来越长,几百毫秒甚至几秒的卡顿在服务端扛不住。

CMS 和 G1 都是冲着这个来的,但走的路不一样:

  • CMS 追求最短停顿,把耗时的标记和清除过程挪到和用户线程并发执行
  • G1 追求停顿时间可预测,在指定的时间预算内挑"最划算"的区域回收

CMS

只收老年代

CMS 全称 Concurrent Mark Sweep,它只管老年代,年轻代得配一个别的收集器,通常是 ParNew。

shell 复制代码
-XX:+UseConcMarkSweepGC      # 老年代用 CMS,年轻代自动配上 ParNew

四个阶段

text 复制代码
1. 初始标记 ( STW )
   只标记 GC Roots 能直接关联到的对象,很快

2. 并发标记
   从这些根出发遍历整个对象图,最耗时,和用户线程并发跑

3. 重新标记 ( STW )
   修正在第 2 步期间用户线程继续运行导致的标记变动,比第 1 步慢,比第 2 步快得多

4. 并发清除
   清掉标记为死亡的对象,和用户线程并发跑

只有第 1 步和第 3 步是停顿的,而且都很短,这就是 CMS 能做到低停顿的原因。

第 3 步为什么必须停顿?因为第 2 步是并发的,用户线程一边跑一边改引用,标记结果会不准,得停下来把这个偏差修正掉。

标记-清除的代价:内存碎片

CMS 用的是标记-清除,不移动对象,所以会产生碎片。

碎片本身不致命,致命的是碎片多了之后,明明老年代还有空间,却没有连续空间放下一个大对象 ,只能提前触发一次 Full GC。而且 CMS 的 Full GC 会退化成 Serial Old 的单线程收集,停顿时间比平时长得多。

参数 -XX:+UseCMSCompactAtFullCollection(默认开启)可以在 Full GC 时做一次压缩整理,但整理是不能并发的,停顿会更长。-XX:CMSFullGCsBeforeCompaction 控制多少次 Full GC 之后压缩一次。

Concurrent Mode Failure

这是 CMS 最容易出问题的地方。

并发清除阶段用户线程还在跑,还在产生新的垃圾,这些新垃圾这一次清不掉,只能留到下一次,这叫浮动垃圾。因为会有浮动垃圾,CMS 不能等老年代快满了才开始收,得提前留出一块空间。

-XX:CMSInitiatingOccupancyFraction 控制触发阈值,默认 92%,意思是老年代用到 92% 就开始 CMS 回收。

如果预留的空间不够(回收速度赶不上分配速度),就会触发 Concurrent Mode Failure。这时 CMS 顶不住了,退化成 Serial Old 做一次全停顿的 Full GC,停顿时间可能是平时的十几倍。日志里看到这个词,基本就是回收速度跟不上分配的征兆。

G1

Region 布局

G1 把堆切成大小相等的 Region(1MB 到 32MB,必须是 2 的幂,通过 -XX:G1HeapRegionSize 指定)。

text 复制代码
   ┌────┬────┬────┬────┬────┬────┬────┬────┐
   │ E  │ E  │ S  │ O  │ O  │ H  │    │ E  │
   └────┴────┴────┴────┴────┴────┴────┴────┘
     E = Eden   S = Survivor   O = Old   H = Humongous

关键点在于这些 Region 逻辑上分代、物理上不连续。年轻代和老年代不再是一整块连续的内存,而是散落在堆各处的 Region 的集合。

这样带来一个 CMS 做不到的能力:回收可以只挑一部分 Region 做 ,不用整个老年代一起收。这是 G1 能做到"可预测停顿"的前提。

超过 Region 大小一半的大对象,会直接放进 Humongous 区域,连续占用若干个 Region。大对象走这条路是因为复制它太贵,G1 干脆让它单独待着,不被复制来复制去。

四个阶段

text 复制代码
1. 初始标记 ( STW )
   标记 GC Roots 直接关联的对象,同时修正 TAMS 指针

2. 并发标记
   从根出发遍历对象图,和用户线程并发

3. 最终标记 ( STW )
   处理并发标记期间记录下来的引用变动

4. 筛选回收 ( STW )
   对每个 Region 按回收价值和成本排序,按停顿目标挑一批出来回收

前三个阶段和 CMS 长得几乎一样,真正的区别在第 4 步。

筛选回收:名字的由来

第 4 步不是把所有垃圾 Region 都收掉,而是先排序再挑:

text 复制代码
对每个 Region 算一个回收价值 = 里面的垃圾占比
按价值从高到低排
在 -XX:MaxGCPauseMillis 允许的时间预算内,从前往后挑 Region 收

-XX:MaxGCPauseMillis 默认 200 毫秒,G1 会基于历史数据预测回收每个 Region 大概要多久,然后挑出一批能在预算内收完的。这就是 "Garbage First" 这个名字的由来:先收垃圾最多的那些 Region。

代价是不能保证每次都能把所有垃圾收干净,垃圾少但还有用的 Region 会留到下一轮。换来的是停顿时间可控。

回收的算法

Region 之间是复制 (把存活对象挪到空 Region 里再整个清空原 Region),Region 内部是整块清理。所以整体上看是标记-整理,不会有 CMS 那样越用越碎的碎片问题。

G1 也有 Full GC,通常在 Mixed GC 跟不上分配速度、或者 Humongous 分配失败时触发。JDK 10 之前它就是 Serial Old 的单线程全停顿,10 之后换成了并行的。

关键差异

对象消失:增量更新 vs SATB

这是两个收集器在实现上最本质的分歧。

并发标记期间用户线程在改引用,会出现两种"标错":

  • 浮动垃圾:标成活的,标完又变成死的。这个无所谓,下次再收
  • 对象消失:标成死的,但它其实是活的。这个会出大事,活对象被当垃圾回收掉

对象消失的触发条件(Wilson 证明的)是两个条件同时成立:

text 复制代码
1. 一个黑色对象(已经标完的)新增了一条指向白色对象的引用
2. 从 GC Roots 到这个白色对象的所有其他引用路径都被断掉了

只要破坏其中任意一个条件就能避免。两个收集器分别选了不同的那个:

CMS G1
手段 增量更新(Incremental Update) 原始快照(SATB)
记什么 黑色对象新增的引用 被删除的旧引用
破坏哪个条件 条件 1 条件 2
重新标记阶段怎么做 以记录下来的黑对象为根,重新扫一遍 以记录下来的旧引用为根,把这些对象当成活的

增量更新的思路是"新增的引用不能漏",所以把新增记录记下来,重新标记时顺着再扫一遍。

SATB( Snapshot At The Beginning ) 的思路完全不同,它是假装对象图停在并发标记开始的那一刻。这期间被删掉的引用不算数,照样按活的算。所以它是记"删除了什么",而不是记"新增了什么"。

SATB 的副作用是产生更多浮动垃圾(明明已经不可达的对象还按活的留着),但换来了重新标记阶段更快,因为不用像 CMS 那样重新扫描一遍子树。

对比表

维度 CMS G1
堆布局 物理上连续的老年代 一堆等大 Region,逻辑分代
回收算法 标记-清除 标记-整理(Region 间复制)
内存碎片 有,碎片多了提前 Full GC 没有
回收粒度 整个老年代 按 Region 挑,收一部分
停顿控制 只能调触发阈值,不能指定目标 -XX:MaxGCPauseMillis 指定目标
大对象 直接进老年代 单独的 Humongous 区域
并发标记 增量更新 SATB
额外内存开销 较少 Remembered Set 等,可能占堆的 1%~20%
适合的堆 中小堆,4~8G 左右 大堆,6G 以上优势明显
JDK 状态 9 标记废弃,14 移除 9 起是默认收集器

怎么选

到了 JDK 14 之后其实没什么可选的,CMS 已经被移除了,G1 是默认值,堆特别大或者对延迟有极端要求的会考虑 ZGC。

如果还在用 JDK 8 纠结这个问题,判断依据是堆大小和延迟要求:

  • 堆在 4~8G 以内,延迟要求不算极端,CMS 够用,调好 CMSInitiatingOccupancyFraction 避开 Concurrent Mode Failure 就行
  • 堆更大(十几 G 以上),或者不想花精力调参、希望有个能指定的停顿目标,选 G1
  • 堆很小(几百 M),G1 的 RSet 开销占比会偏高,反而不如 CMS 或者 Parallel

需要注意 G1 的 MaxGCPauseMillis 设得太激进(比如 20ms)会适得其反:预算是死的,回收的 Region 就少,垃圾清理跟不上,Mixed GC 频繁触发,反而更慢。

相关推荐
用户6802659051191 小时前
企业电脑统一管理怎么做?2026企业终端统一管理方法与工具推荐
javascript·后端·面试
青Cheng序员石头1 小时前
失控之前 | AI 安全到底在保护什么?
后端·安全·aigc
二月龙1 小时前
为什么很多大模型 Demo 看着很强,上线业务就翻车
后端
XuCoder1 小时前
Spring Boot 报端口被占用,netstat 却查不到进程,谁在抢?
后端
仿生狮子1 小时前
实现近乎免费之后,设计工程师还剩什么
前端·后端·设计
yunwei371 小时前
使用 eBPF 跟踪 Nginx 请求
linux·后端·性能优化
yunwei371 小时前
使用 eBPF 跟踪 MySQL 查询
linux·后端·性能优化
yunwei371 小时前
eBPF 示例教程:使用 XDP 捕获 TCP 信息
linux·后端·性能优化
用户8356290780511 小时前
Python 设置 Excel 单元格边框与样式
后端·python