G1收集器通过Remembered Set(RSet)避免扫描整个老年代:RSet是每个Region维护的跨区引用账本,由写屏障实时更新,记录哪些老年代Region引用了当前新生代Region中的对象;Minor GC时将RSet中登记的外部Region作为额外GC Roots,仅据此标记存活对象,大幅缩减扫描范围。

G1 收集器不扫描整个老年代回收新生代,核心靠的是 Remembered Set(RSet) —— 它不是辅助手段,而是每个 Region 的“跨区引用账本”。只要老年代某个对象引用了当前新生代 Region 中的对象,这条引用就会被记录在该新生代 Region 对应的 RSet 里。
Remembered Set 是怎么建起来的?
RSet 不是靠扫描生成的,而是靠写屏障(Write Barrier)实时维护的:
- 每当发生一次跨 Region 的引用写入(比如老年代 Region A 中的对象 obj.field = newInEden),JVM 在执行赋值前会触发写屏障;
- 写屏障立即定位到被引用对象所在的 Region(这里是 Eden 区某 Region),并把“Region A → 这个 Eden Region”的关系登记进该 Eden Region 的 RSet;
- RSet 内部通常用哈希表或位图实现,支持快速插入和查重,避免重复记录同一源 Region。
Minor GC 时 RSet 怎么用?
年轻代 GC(即 G1 的 Young GC)只处理 Eden 和 Survivor Region。此时流程是:
- 枚举 GC Roots(线程栈、本地变量、JNI 引用等);
- 额外把所有待回收 Eden Region 的 RSet 中记录的“外部引用源”也加入 Roots 集合(例如:RSet 里存着 Region X、Y、Z,就把它们里面的存活对象当作 Roots 的一部分);
- 然后仅从这些 Roots 出发,标记当前 Eden/Survivor 中的存活对象——完全不需要遍历老年代内存。
为什么 RSet 能保证不漏标?
关键在于写屏障的覆盖性和 RSet 的粒度控制:
- 所有可能产生跨 Region 引用的写操作(包括数组元素赋值、对象字段赋值、volatile 写等)都会被写屏障拦截;
- G1 的 RSet 按 Region 维护,比 CMS 的卡表(Card Table)更精细:卡表以 512 字节为单位标记“脏页”,而 RSet 直接记录“哪个 Region 引用了我”,省去后续解析脏页的开销;
- 配合增量更新(如 G1 的 Dirty Card Queue + 并发 Refine 线程),RSet 可在后台持续刷新,避免 GC 时集中处理导致 STW 延长。
实际效果:RSet 让扫描范围从“整个老年代”压缩到“几十个 Region 头”
假设堆有 2048 个 Region,其中老年代占 1500 个。一次 Young GC 若需全扫老年代,要检查上亿字节;但启用 RSet 后,平均只需检查几十个 Region 的对象头(每个 Region 的 RSet 条目通常只有几个到几十个),GC 根集合大小下降 2–3 个数量级,STW 时间稳定在毫秒级。

















