JVM G1 CMS 垃圾收集器工作流程简化流程图

G1 的流程

bash 复制代码
+------------------------------------+
|            触发条件                |
| - 内存使用达到阈值                 |
| - 系统调用                         |
+-----------------+------------------+
                  |
                  v
+-----------------+------------------+
|        初始标记 (STW)              |
| - 标记 GC Roots 引用的对象         |
| - 暂停时间较短                     |
+-----------------+------------------+
                  |
                  v
+-----------------+------------------+
|       并发标记 (无 STW)            |
| - 遍历对象图                       |
| - 标记所有可达对象                 |
| - 记录引用变更                     |
+-----------------+------------------+
                  |
                  v
+-----------------+------------------+
|       最终标记 (STW)               |
| - 处理引用变更                     |
| - 完成所有存活对象的标记           |
| - 暂停时间较短                     |
+-----------------+------------------+
                  |
                  v
+-----------------+------------------+
|      筛选回收 (部分 STW)           |
| - 选择高收益 Region                |
| - 清理选定 Region                  |
| - 移动存活对象                     |
+-----------------+------------------+
                  |
                  v
+-----------------+------------------+
|             结束                   |
+------------------------------------+
bash 复制代码
[开始 Young GC]
│
├─ ⏸️ 初始标记(STW 1ms)  
│    → 找到 GC Roots(比如栈里的 user 变量)
│
├─ ▶ 并发标记(200ms,程序正常跑!)  
│    → GC 线程从 user 出发,标记 User → Order → Item...  
│    → 你的程序同时在处理新订单、改地址......  
│    → 每次改引用,写屏障默默记录  
│
├─ ⏸️ 最终标记(STW 2ms)  
│    → 检查写屏障日志:"哦,有个 Address 被断开了?赶紧标上!"  
│
└─ ▶ 筛选回收  
     → 把垃圾最多的 Region 清掉,活对象搬走

G1 的核心流程口诀
"一标(初始)停,二标(并发)跑,
三标(最终)停,四筛(部分 STW)扫。
整理移,少碎片,
高效收,暂停少。"

CMS 的流程

bash 复制代码
+------------------------------------+
|            触发条件                |
| - 老年代使用达到阈值              |
| - 系统调用                         |
+-----------------+------------------+
                  |
                  v
+-----------------+------------------+
|        初始标记 (STW)              |
| - 标记 GC Roots 引用的对象         |
| - 暂停时间较短                     |
+-----------------+------------------+
                  |
                  v
+-----------------+------------------+
|       并发标记 (无 STW)            |
| - 遍历对象图                       |
| - 标记所有可达对象                 |
| - 记录引用变更                     |
+-----------------+------------------+
                  |
                  v
+-----------------+------------------+
|       重新标记 (STW)               |
| - 处理引用变更                     |
| - 完成所有存活对象的标记           |
| - 暂停时间较短                     |
+-----------------+------------------+
                  |
                  v
+-----------------+------------------+
|       并发清除 (无 STW)            |
| - 回收未标记的垃圾对象             |
| - 不移动对象,只释放内存           |
+-----------------+------------------+
                  |
                  v
+-----------------+------------------+
|             结束                   |
+------------------------------------+

CMS 的核心流程口诀
"一标(初始)停,二标(并发)跑,
三标(重新)停,四清(并发)扫。
不整理,有碎片,
卡顿时,Full GC 来救场。"

CMS + ParNew 的整体 Young/Old 回收配合

区域 回收器 算法 是否 STW
年轻代 ParNew 多线程标记-复制 ✅ 全程 STW(但很快)
老年代 CMS 并发标记-清除 ⏸️ 仅初始+重新标记 STW
回收器 年轻代算法 老年代算法 是否分代
CMS ParNew(多线程复制) Concurrent Mark-Sweep(并发标记-清除) ✅ 是
G1 G1 Young GC(分区复制) G1 Mixed GC(分区复制 + 并发标记) ✅ 是(但弱化分代)
维度 CMS G1
年轻代算法 ParNew(多线程复制) G1 Young GC(分区复制)
老年代算法 CMS(并发标记-清除) G1 Mixed GC(并发标记-分区复制)
是否移动对象 ❌ 老年代不移动 ✅ 全堆移动(复制)
内存碎片 ✅ 严重(老年代) ❌ 无
Full GC 风险 ⚠️ 高(碎片导致) ✅ 极低
停顿可预测性 中(Remark 阶段可能较长) 高(可设 MaxGCPauseMillis)
大堆支持 差(>64GB 易失控) 优秀(TB 级)
对比维度 CMS G1
设计目标 最小化老年代停顿时间 可控停顿 + 高吞吐 + 无碎片
内存模型 严格分代: Eden / Survivor / 老年代(连续) 分区(Region): 堆划分为等大 Region,可动态扮演 Eden/Survivor/Old
回收范围 只回收老年代 (年轻代靠 ParNew) 混合回收: 一次 GC 可同时回收年轻代 + 部分老年代
是否整理内存 ❌ 不整理 → 内存碎片严重 ✅ 整理(Region 间复制)→ 无碎片
写屏障用途 记录新增引用(增量更新) 记录跨 Region 引用(维护 Remembered Set)
漏标解决方案 增量更新(Incremental Update) → 重新扫描新引用 SATB(Snapshot-At-The-Beginning) → 保留"开始时"的快照,记录消失的引用
Full GC 风险 ⚠️ 高(碎片导致) ✅ 极低(自动避免碎片)
是否废弃 ✅ JDK 14 移除 ✅ JDK 9+ 默认回收器

1️⃣ 内存布局:分代 vs 分区
CMS:老年代是一整块连续内存。清除后留下"洞",大对象可能放不下。
G1:堆被切成 2048 个左右的 Region(默认 1~32MB),每个 Region 独立管理。
→ 回收时只选"垃圾最多的几个 Region",局部整理,全局无碎片。
🧩 就像:
CMS:清理整栋老楼,但房间大小不一,留空隙
G1:把城市分成小区,每次只拆重建几个最破的小区

屏障的作用?

2️⃣ 屏障(Barrier)虽然都有,但干的事不同!

✅ CMS 的写屏障:监控"新引用"

当代码执行 A.ref = B(A 在老年代,B 在老年代),

写屏障会记录:"A 现在引用了 B"

在 重新标记阶段,CMS 会重新扫描这些"新引用",防止漏标

🎯 目标:确保新产生的引用被标记到

✅ G1 的写屏障:监控"跨 Region 引用"

G1 的核心难题:一个 Region 里的对象,可能被其他 Region 引用

写屏障的作用:当 A(在 R1).ref = B(在 R2),

自动在 R2 的 Remembered Set(记忆集) 中记录:"R1 引用了我"

Young GC 时,只需扫描 Eden 区 + 被引用的老年代 Region 的 Remembered Set,不用扫整个老年代!

🎯 目标:加速根扫描,避免全堆遍历

💡 所以:CMS 的屏障为"标记正确性"服务,G1 的屏障为"分区效率"服务。

最终清理(如回收内存)虽然可能部分并发,但关键操作(如释放内存块、更新空闲列表)通常发生在"安全点"或短暂 STW 阶段,确保应用线程不会访问即将被回收的对象。

CMS 清垃圾,不动活对象;
死者无人问,清扫可并发;
只怕碎片多,Full GC 来救场!

🎯 核心原则:宁可错杀(多标),不可漏杀(少标)

"标",指的是「标记活着的对象」。

那怎么实现的不漏标呢?
CMS 如何防止?→ 增量更新(Incremental Update)

CMS 的思路是:

"只要引用关系变了,我就重新扫描源头!"

具体做法:

当发生 C.ref = B(老年代内部引用变更)

写屏障记录:"C 新增了一个引用"

在 重新标记阶段(STW),CMS 会:

把所有"新增引用的源头"(如 C)重新加入扫描队列

从 C 出发,再次遍历 → 发现 B → 标记 B 为活

✅ 结果:B 不会被漏掉
🧠 类比:
老师点名时,班长报告:"刚才小明坐到了第三排!"
老师立刻回头确认:"第三排的小明,到!" → 不会漏点。

⚠️ 但 CMS 有个弱点:

如果 A.ref = null 导致 B 只被 C 引用,而 C 本身是活的,CMS 能靠"新增引用"发现 B。

但如果引用是 断开 + 重建 的复杂链,理论上仍有极小风险(实践中极少发生)。
G1 如何防止?→ SATB(Snapshot-At-The-Beginning)

G1 的思路更激进:

"我不管中间怎么变,只要在标记开始时它是活的,我就保它到底!"

具体做法:

在 初始标记阶段,拍一张"快照":记录当时所有存活对象

当发生 A.ref = null(断开对 B 的引用)

写屏障捕获这个"消失的引用"

把 B 加入待处理队列(即使 B 后来被 C 引用,也不影响)

在 最终标记阶段,强制标记 B 为活

✅ 结果:B 绝对不会被回收

🧠 类比:
上课铃响时(标记开始),小明在教室 → 老师说:"今天小明算到课!"
即使小明中途溜去厕所(A 不再指向 B),甚至被隔壁班拉去帮忙(C 指向 B),
老师依然认为他到课了 ------ 因为铃响时他在!

💡 SATB 的优势:天然防漏标,即使引用链断裂又重建,也不怕。
ZGC / Shenandoah:更极致的保障

它们用 读屏障 + 转发指针:

只要你的程序试图访问一个对象,

读屏障会检查它是否已被标记为"待回收"

如果是,就自动阻止回收或重定向

✅ 相当于:"只要你还想用,我就不能收!"

阶段 G1 CMS
标记阶段是否需要 STW? ✅ 是(最终标记) ✅ 是(重新标记)
清理/清除阶段是否需要 STw? ⚠️ 部分需要(复制存活对象) ❌ 不需要(纯并发)
为什么清理能并发? 因为要移动对象,需同步 因为只释放死对象,而死对象已不可达

🔑 关键区别:G1 要"移动活对象",CMS 只"释放死对象"。

问题 CMS G1
是否有 STW? ✅ 有(初始标记 + 重新标记) ✅ 有(初始标记 + 最终标记)
清除/回收阶段是否并发? ✅ 是(只释放死对象) ⚠️ 部分并发(复制活对象需同步)
为什么清除能并发? 死对象已不可达,程序碰不到 ------
如果程序突然引用一个"死对象"? 不可能!因为那就说明它不是死的 同上
最大风险 内存碎片 → Full GC 混合回收停顿略长
回收器 如何处理死对象? 内存布局变化 是否产生碎片
CMS 标记后直接释放死对象的内存块 (就地清除) 老年代仍是一整块,但内部有"空洞" ✅ 严重碎片
G1 不处理死对象!只把活对象复制走 (整 Region 丢弃) 源 Region 变为空闲,可复用 ❌ 无碎片

🔑 G1 根本不在乎"死对象在哪"------它只关心"活对象去哪"。
🧠 类比:
CMS:打扫房间,把垃圾一件件扔出去,但家具位置不动 → 地板有空隙
G1:直接把整间脏屋子(Region)废弃,搬进新装修的房子 → 旧屋子推平重建

相关推荐
heartbeat..4 小时前
Redis 中的锁:核心实现、类型与最佳实践
java·数据库·redis·缓存·并发
2301_790300964 小时前
Python单元测试(unittest)实战指南
jvm·数据库·python
4 小时前
java关于内部类
java·开发语言
好好沉淀4 小时前
Java 项目中的 .idea 与 target 文件夹
java·开发语言·intellij-idea
gusijin4 小时前
解决idea启动报错java: OutOfMemoryError: insufficient memory
java·ide·intellij-idea
To Be Clean Coder4 小时前
【Spring源码】createBean如何寻找构造器(二)——单参数构造器的场景
java·后端·spring
吨~吨~吨~4 小时前
解决 IntelliJ IDEA 运行时“命令行过长”问题:使用 JAR
java·ide·intellij-idea
你才是臭弟弟4 小时前
SpringBoot 集成MinIo(根据上传文件.后缀自动归类)
java·spring boot·后端
短剑重铸之日4 小时前
《设计模式》第二篇:单例模式
java·单例模式·设计模式·懒汉式·恶汉式
码农水水4 小时前
得物Java面试被问:消息队列的死信队列和重试机制
java·开发语言·jvm·数据结构·机器学习·面试·职场和发展