RSet是G1收集器中每个Region独立维护的堆外反向索引结构,精准记录跨Region引用来源,使Young GC仅扫描少量脏卡而非全老年代,从而实现停顿时间与堆大小解耦。

Remembered Set(RSet)是 G1 收集器实现低延迟回收的关键基础设施,它不解决“我引用了谁”,而是精准回答“谁引用了我”——尤其聚焦于跨 Region 的反向引用,让年轻代 GC 能跳过全量老年代扫描。
RSet 的本质:每个 Region 自带的反向索引本
RSet 不是全局大表,也不是 Card Table 的复制品,而是一个 Region 级别的、堆外存储的哈希表 + 卡页位图结构。它只登记“哪些其他 Region 的哪些卡页里,存在指向本 Region 的引用”。例如:Region A 中的对象被 Region B(老年代)中的对象引用,那么 Region A 的 RSet 就会记录一条“B→A”的映射,并标记 B 中对应卡页为脏。
- 每个 Region 独立维护自己的 RSet,互不共享
- 仅记录跨 Region 引用,同 Region 内引用从不进入 RSet
- 只追踪老年代 → 年轻代/其他老年代的写入,年轻代 → 老年代的赋值不触发 RSet 更新
- RSet 内存分配在堆外(HotSpot 的 Native Memory),不参与 GC 统计,但真实占用 RSS
为什么必须有 RSet:避免 Young GC 停顿失控
没有 RSet,G1 在年轻代回收时就无法知道哪些老年代对象正持有对 Eden/Survivor 中对象的强引用。只能退化为把整个老年代加入 GC Roots——停顿时间随堆大小线性增长,彻底丧失低延迟特性。
- Young GC 实际只扫描 Eden/Survivor Region 对应的 RSet,从中提取出“可能持有引用”的老年代 Region 和脏卡范围
- 扫描粒度从“全老年代”降为“少量脏卡”,使停顿时间与堆总大小解耦
- 混合回收(Mixed GC)同样依赖 RSet,精准定位待回收 Old Region 的外部引用来源
RSet 如何更新:写屏障驱动,非实时同步
RSet 更新完全由 G1PostBarrier 写屏障触发,不是自动或周期性行为。每次发生跨 Region 赋值(如 oldObj.field = youngObj),JVM 插入检查逻辑:
- 判断目标字段所在 Region 与新值所在 Region 是否不同
- 若跨 Region,定位源 Region(oldObj 所在)的卡页索引(默认 512 字节/卡)
- 将该卡标记为 dirty,并交由并发 Refine 线程异步处理,最终注册进目标 Region(youngObj 所在)的 RSet
这个过程有开销:高频写入(如缓存批量更新)会导致 dirty card 积压、Refine 线程跟不上,进而引发 Concurrent Mode Failure 或 RSet 扫描滞后。
RSet 的调优关键点:粒度、密度与节奏平衡
RSet 性能瓶颈往往来自三方面失衡:Region 太小导致实例爆炸、跨 Region 引用太密导致哈希冲突、Refine 线程太少导致更新延迟。
- Region Size 选择:默认 2MB;堆 ≥32GB 可设为 4M 或 8M;避免设为 512KB(RSet 实例翻倍,堆外内存碎片激增)
- 控制引用密度:避免老年代长期持有大量新生代临时对象(如 Map<String, List<DTO>> 中 DTO 分配在 Eden)
- 提升 Refine 吞吐:增加 -XX:G1ConcRefinementThreads(默认 6,高写场景可设为 12–16)
- 监控信号:jstat -gc 中 CCSU(Concurrent RS Update)占比持续 >10%,或 GC 日志中 [RSet: XMB, Y entries] 后 [scan: Zms] >20ms,即需干预

















