RSet的核心作用是避免全局扫描:它将“哪些Region引用了我”固化在每个Region元数据中,Young GC仅需检查目标Region的RSet即可识别跨区存活对象,结合写屏障维护和Card Table粗粒度索引,实现毫秒级停顿。

RSet 的核心作用不是“记录引用”,而是让 G1 在回收某个 Region 时,完全跳过对其他 Region 的全局扫描。 它把“哪些 Region 引用了我”这个信息固化在每个 Region 自身的元数据里,使 Young GC 或 Mixed GC 只需检查目标 Region 的 RSet,就能安全识别跨代/跨区存活对象。
为什么 Young GC 不用扫老年代?靠的是 RSet + 写屏障
传统分代 GC(如 Parallel GC)做 Young GC 时,必须扫描整个老年代,确认有没有老对象引用了新生代对象——否则会误回收。G1 把这件事交给每个 Region 自己的 RSet 来承载:
- 每次执行
obj.field = otherObj这类写引用操作时,JVM 插入写屏障(Write Barrier) - 写屏障判断
obj和otherObj是否在不同 Region;若在,就把这次引用关系记入otherObj所在 Region 的RSet - Young GC 开始时,只扫描 Eden/Survivor Region 的
RSet,就知道哪些老 Region 有指向本 Region 的引用 - 这些老 Region 的 RSet 条目会被加入 GC Roots,后续仅遍历这些条目对应的老对象即可
这直接切断了 Young GC 时间与老年代大小的耦合关系——哪怕堆是 64GB,只要活跃跨区引用少,Young GC 停顿仍可控制在毫秒级。
RSet 存储结构不是全对象地址,而是基于 Card Table 的粗粒度索引
RSet 本身不存具体字段或对象指针,它背后依赖 Card Table 做精度折中:
- 堆被划分为 512 字节一张的 card;每张 card 对应一个 bit 标记是否被写过
- 写屏障只标记“某张 card 被修改”,不记录改了哪个字段、指向谁
- Region 的
RSet实际维护的是“哪些 Region 的哪些 card 里可能含有指向本 Region 的引用” - GC 扫描时,先定位到相关 card,再在 card 内部做精确扫描(类似局部遍历)
这种设计大幅降低写屏障开销和 RSet 内存占用,但也意味着 RSet 是“保守近似”——可能多扫几张 card,但绝不会漏扫。
RSet 大小直接影响 GC 停顿,但不能靠调参强行压缩
RSet 占用堆外内存(off-heap),大小取决于跨 Region 引用密度。常见问题包括:
- 应用存在大量长生命周期对象持有短生命周期对象(如缓存 map 持有临时 DTO),导致 RSet 膨胀
-
-XX:G1RSetSparseRegionEntries和-XX:G1RSetRegionEntries控制 RSet 的稀疏/密集阈值,但设得太低会导致频繁扩容/合并,反而增加 STW 时间 - 使用
jstat -gc <pid>查看RS列(Remembered Set size),持续增长且远超EU(Eden Used)时需警惕 - RSet 数据本身不参与三色标记,但它的扫描阶段在初始标记(Initial Mark)和并发标记后(Remark)都会触发,是 STW 关键路径之一
真正可控的优化点不在 RSet 参数,而在减少跨 Region 引用本身:比如避免老 Region 中的集合类长期持有新生代对象,或用弱引用/软引用替代强引用。
RSet 看似是 G1 的内部实现细节,但它决定了你能否真正获得“停顿时间可预测”——而这个预测能力,本质上来自对跨 Region 引用边界的显式建模,而不是靠扫描范围妥协换来的模糊上限。

















