rset是专为解决“谁引用了我”的跨region可达性问题设计的反向索引结构,其更新依赖写屏障触发、dirty card queue缓冲及g1concrefinementthreads异步精炼协同;仅老→年或跨老region写操作触发更新,年轻代→老年代赋值被跳过;队列满时应用线程阻塞属背压保护,非缺陷;rset膨胀主因region过小或引用密度过高,调优应优先收敛引用模型,再合理设置regionsize(4m–8m)和refinement线程数。

RSet(Remembered Set)不是为“记录我引用了谁”而存在,而是专为解决“谁引用了我”这一跨 Region 可达性问题设计的反向索引结构。它的更新机制直接决定 Young GC 的停顿稳定性与 Mixed GC 的回收准确性,核心不在实时性,而在写屏障触发、缓冲队列承载、异步精炼三者的协同节奏。
写屏障:RSet更新的唯一入口
只有老年代对象字段指向年轻代或其它老年代 Region 时,G1PostBarrier 才会介入。伪代码逻辑为:
- 若 field 所在 Region 是老年代,且 new_val 所在 Region 是年轻代(或不同老年代 Region),则触发 RSet 更新
- 定位 new_val 所属 Region 的 RSet
- 计算 field 地址对应的老年代卡页(Card,512 字节为单位),标记该卡为 dirty
- 将此 dirty 卡注册进目标 Region 的 RSet 映射表中
年轻代 → 老年代的赋值被完全跳过——这类引用不参与年轻代存活判定,且随 Minor GC 大量消亡,记录性价比极低。
Dirty Card Queue:写与修之间的缓冲带
写屏障不直接修改 RSet,而是把 dirty 卡信息先写入线程本地的 Dirty Card Queue(DCQ)。这是关键缓存层,作用是解耦 mutator 线程与 RSet 构建开销:
- 每个应用线程独享一个 DCQ,容量默认有限(受 -XX:G1ConcRefinementGreenZone 等阈值控制)
- 队列满时,线程本地 DCQ 被推入全局 Dirty Card Queue Set(DCQS)等待处理
- 若突发写压导致 DCQ 持续满载,mutator 线程会在写屏障中短暂阻塞,等待 Refinement 线程腾出空间
这不是设计缺陷,而是背压保护机制:宁可让少量写延迟,也不让 RSet 更新滞后引发漏标。
并发优化线程:RSet 的“后台精炼师”
RSet 的真实构建由 G1ConcRefinementThreads 异步完成。它们持续从 DCQS 中拉取 completed buffer,执行三步操作:
- 扫描 dirty 卡:遍历卡内所有对象,检查其字段是否真指向目标 Region
- 聚合反向引用:统计哪些源 Region 向本 Region 写入了有效引用
- 更新哈希表:将源 Region ID 注册进目标 Region 的 RSet,支持 O(1) 查询
这个过程非原子、非即时。若 Refinement 线程跟不上写节奏(如 CCSU 时间占比 >10%),Mixed GC 开始时 RSet 就可能缺失最新引用,导致 evacuation 失败或被迫扩大根集合。
RSet膨胀与调优的关键抓手
RSet 内存占用不可低估,它分配在堆外(Native Memory),GC 日志里不可见,但 jcmd VM.native_memory 或 jstat -gc 中的 RS 列可反映其规模:
- Region 太小(如 512KB)→ Region 数暴增 → RSet 实例数翻倍 → 堆外内存碎片+哈希冲突加剧
- 引用模式密集(如一个缓存节点被数百个老 Region 指向)→ 单 Region RSet entries 超 10 万 → 插入退化为 O(n)
- 观察 GC 日志中 Update RS (ms):Avg >10ms 或 Max >30ms,且伴随 Ext Root Scanning 上涨,即为瓶颈信号
调优优先级应是:收敛业务引用模型(避免老对象高频改写新生代引用)>合理设置 -XX:G1HeapRegionSize(推荐 4M–8M)>按需调整 -XX:G1ConcRefinementThreads(通常设为 CPU 核数/2,上限 8)。











