G1 深入:Region、Remembered Set、三色标记与“可预测停顿”

G1 深入:Region、Remembered Set、三色标记与“可预测停顿” 你如果在线上遇到过这类问题接口 RT 偶尔抖一下P99/P999 很难稳Full GC 偶发一次停顿几秒甚至十几秒堆不小但又不敢把 STW 拉太长那你问“为什么选 G1”本质是在问我能不能让 GC 停顿更可控我能不能用更小粒度回收而不是动不动整代回收G1 之所以重要是因为它把“回收粒度”和“停顿目标”变成了可解释、可观测、可调的工程问题。这篇按一条主线把 G1 串起来Region把堆切碎回收粒度变小RSet解决跨 Region 引用保证回收正确性并发标记三色标记 SATB 写屏障让标记尽量不阻塞业务停顿预测按收益/成本挑回收集合让停顿落在目标内1. Region为什么要把堆切碎传统分代是“连续的大块空间”而 G1 用 Region 化的意义是回收不再只能按“整代”进行可以挑选收益高的 Region 回收为可预测停顿提供基础你可以把 Region 理解成堆的最小管理单元一个非常关键的工程意义G1 不是只能按“整代”回收它可以按“哪些 Region 值得回收”来回收这为“可预测停顿”提供了基础2. 跨 Region 引用为什么一定需要 RSet以及它的代价Region 化后会出现一个问题A Region 里的对象引用了 B Region 里的对象当你只回收 B Region 时你必须知道是否有别的 Region 在引用 B Region 里的对象否则会误回收。这就是 Remembered SetRSet的作用记录“哪些 Region 引用了我”工程上你要知道的代价RSet 会占用额外内存一定会吃掉一部分堆外/堆内元数据开销依实现而定维护 RSet 需要写屏障开销每次写引用都可能要更新相关记录为了把 RSet 的维护做得更可控G1 通常还会配合类似“卡表Card Table”这样的粗粒度记账方式把内存划成更小的卡片card写屏障把“某个区域的某张卡被写过”记录下来后续回收时再基于这些记录去更新/扫描3. 并发标记三色标记 SATB 写屏障你要理解的不是名词而是矛盾三色标记是一个思维模型白色未访问可能是垃圾灰色已发现但子引用未完全扫描黑色已扫描完认为存活并发标记时难点在于标记线程在遍历对象图的同时业务线程仍在修改引用关系这就是为什么 G1以及很多并发收集器会引入写屏障Write Barrier在“写引用”发生时插入一小段逻辑维护并发标记需要的辅助信息SATBSnapshot-At-The-Beginning以“标记开始时的对象图快照”作为基准处理并发修改带来的不一致你不用背到源码级但要会解释清楚并发标记要解决“对象图在变化”的一致性问题否则会出现误标/漏标写屏障与 SATB 是为了在可控成本下把并发标记变成“结果正确”4. G1 的回收主流程Young GC、并发标记、Mixed GC只说“G1 把堆切成 Region”是不够的你需要能把主流程讲清楚。你可以按这个顺序理解Young GC年轻代回收优先回收年轻代 Region停顿相对短并发标记Concurrent Mark标记存活对象为后续 Mixed 回收提供依据Mixed GC混合回收把一部分“收益高”的老年代 Region 加入回收集合这就是为什么很多在线服务会选择 G1年轻代回收保持高频低成本老年代回收不一定要等到非常满才来一次很重的 Full GC而是能被 Mixed 分摊5. 停顿预测G1 的“可控”从哪里来G1 的目标是尽量满足你设定的停顿目标比如 200ms它的做法大致是统计每个 Region 的回收收益可回收垃圾比例与成本预计回收耗时在一次回收中选择一批 Region称为回收集合让这批 Region 的回收成本尽量落在停顿预算内因此你看到的工程现象G1 的停顿相对更“可预测”但不是绝对保证代价是更复杂的管理结构Region/RSet/写屏障6. 工程上怎么观测与排障先会看现象再谈“调参”G1 调参之前你至少要能回答现在卡顿来自 Young GC 还是 Mixed GC停顿时间主要花在“扫描引用”还是“对象搬迁Evacuation”是不是存在晋升失败、老年代增长过快、或回收跟不上分配6.1 你可以先用这些命令做低侵入确认查看堆结构jcmd pid GC.heap_info查看线程是否 STW 后堆栈堆在一起jcmd pid Thread.print卡顿时抓多次对比查看对象大户jcmd pid GC.class_histogram6.2 如果有 GC 日志你重点盯这些关键词Pause Young (Normal)年轻代回收Pause Young (Mixed)混合回收年轻代 部分老年代 RegionEvacuation Failure/to-space exhausted搬迁空间不够危险信号你不需要一开始就把每个字段都读懂但要形成“把一次停顿拆成模块”的习惯哪一段耗时最大是不是某类对象导致搬迁成本过高7. 常见调参思路只讲工程上常用、且容易解释清楚的这部分我建议你把“目标 - 动作 - 代价”讲出来而不是背参数。7.1 目标停顿更可控动作设置一个合理的停顿目标例如-XX:MaxGCPauseMillis200代价为了满足停顿目标G1 可能更频繁地回收吞吐会有损失7.2 目标减少 Mixed/Old 压力动作关注晋升是否过快、对象是否真的长期存活缓存/集合/大对象代价本质往往是业务对象生命周期问题不是单靠参数能解决8. 总结你讲得清楚 G1就讲得清楚“为什么要它”Region把回收粒度切小支持按收益选择回收集合RSet解决跨 Region 引用保证回收正确性但有内存与写屏障代价并发标记三色标记 SATB 写屏障是为了解决“对象图在变化”的一致性回收主流程Young GC - 并发标记 - Mixed GC尽量避免一次巨大的 Full GC停顿预测基于收益/成本模型选择回收集合让停顿更接近目标