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 Set(RSet)的作用:

  • 记录"哪些 Region 引用了我"

工程上你要知道的代价:

  • RSet 会占用额外内存(一定会吃掉一部分堆外/堆内元数据开销,依实现而定)
  • 维护 RSet 需要写屏障开销(每次写引用,都可能要更新相关记录)

为了把 RSet 的维护做得更可控,G1 通常还会配合类似"卡表(Card Table)"这样的粗粒度记账方式:

  • 把内存划成更小的卡片(card)
  • 写屏障把"某个区域的某张卡被写过"记录下来
  • 后续回收时再基于这些记录去更新/扫描

3. 并发标记:三色标记 + SATB + 写屏障(你要理解的不是名词,而是矛盾)

三色标记是一个思维模型:

  • 白色:未访问(可能是垃圾)
  • 灰色:已发现但子引用未完全扫描
  • 黑色:已扫描完(认为存活)

并发标记时,难点在于:

  • 标记线程在遍历对象图的同时,业务线程仍在修改引用关系

这就是为什么 G1(以及很多并发收集器)会引入:

  • 写屏障(Write Barrier):在"写引用"发生时插入一小段逻辑,维护并发标记需要的辅助信息
  • SATB(Snapshot-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_histogram

6.2 如果有 GC 日志,你重点盯这些关键词

  • Pause Young (Normal):年轻代回收
  • Pause Young (Mixed):混合回收(年轻代 + 部分老年代 Region)
  • Evacuation Failure / to-space exhausted:搬迁空间不够(危险信号)

你不需要一开始就把每个字段都读懂,但要形成"把一次停顿拆成模块"的习惯:

  • 哪一段耗时最大
  • 是不是某类对象导致搬迁成本过高

7. 常见调参思路(只讲工程上常用、且容易解释清楚的)

这部分我建议你把"目标 -> 动作 -> 代价"讲出来,而不是背参数。

7.1 目标:停顿更可控

  • 动作:设置一个合理的停顿目标(例如 -XX:MaxGCPauseMillis=200
  • 代价:为了满足停顿目标,G1 可能更频繁地回收,吞吐会有损失

7.2 目标:减少 Mixed/Old 压力

  • 动作:关注晋升是否过快、对象是否真的长期存活(缓存/集合/大对象)
  • 代价:本质往往是业务对象生命周期问题,不是单靠参数能解决

8. 总结:你讲得清楚 G1,就讲得清楚"为什么要它"


  • Region:把回收粒度切小,支持按收益选择回收集合
  • RSet:解决跨 Region 引用,保证回收正确性(但有内存与写屏障代价)
  • 并发标记:三色标记 + SATB + 写屏障,是为了解决"对象图在变化"的一致性
  • 回收主流程:Young GC -> 并发标记 -> Mixed GC,尽量避免一次巨大的 Full GC
  • 停顿预测:基于收益/成本模型选择回收集合,让停顿更接近目标
相关推荐
sprite_雪碧2 小时前
简单模拟问题
算法
她说彩礼65万2 小时前
C语言 Static的用法
java·linux·c语言
2401_874732532 小时前
C++中的装饰器模式
开发语言·c++·算法
j_xxx404_2 小时前
力扣--分治(快速排序)算法题II:数组中的第K个最大元素(Top K问题),LCR159.库存管理III
数据结构·c++·算法·leetcode
ysa0510302 小时前
运用map优化多次查询【Kadomatsu 子序列】
数据结构·c++·笔记·算法
dapeng28702 小时前
机器学习与人工智能
jvm·数据库·python
spencer_tseng2 小时前
java.lang.ClassNotFoundException: org.slf4j.Logger
java·spring·maven
_饭团2 小时前
C 语言内存函数全解析:从 memcpy 到 memcmp 的使用与模拟实现
c语言·开发语言·c++·学习·算法·面试·改行学it
24白菜头2 小时前
第十五届蓝桥杯C&C++大学B组
数据结构·c++·笔记·学习·算法·leetcode·蓝桥杯