跳转到主内容
极星编程网:以代码为星,赴技术山海!

如何分析 G1 GC 的 RSet(Remembered Set)更新缓存区对应用突发写操作产生的性能背压

Dirty Card Queue 成为突发写瓶颈,因大量跨 Region 写操作瞬间填满队列,导致应用线程阻塞或 GC 线程拖慢,进而引发 RSet 更新滞后与 Mixed GC 识别错误;可通过 GC 日志中 Update RS 耗时(Avg >10ms 或 Max >30ms)及 Ext Root Scanning 上涨定位,优化需结合调参(如 G1ConcRefinementThreads、G1RSetUpdatingPauseTimePercent)与写模式治理(对象复用、预分配、拆分大对象)。 为什么 RSet 更新缓存区(Dirty Card Queue)会成为突发写操作的瓶颈 G1 的
RSet
本质是每个 Region 对应的“谁引用了我”的反向索引,靠写屏障(Write Barrier)在每次跨 Region 写操作时记录脏卡(dirty card)。这些记录不直接刷入 RSet,而是先攒进线程本地的
Dirty Card Queue
—— 这就是缓存区。当队列满、或 GC 前/中触发刷新时,才批量处理并更新 RSet。突发写(如批量导入、消息积压消费、聚合计算)会导致大量跨 Region 引用,瞬间填满队列,进而引发两种背压: 应用线程被阻塞在写屏障里等待队列腾出空间 ,或 GC 线程被拖慢,因 RSet 更新滞后导致 Mixed GC 无法及时识别老年代存活对象 。 如何定位 Dirty Card Queue 溢出是否正在发生 关键看 GC 日志里是否频繁出现
DirtyCardQueueSet::apply_closure_to_completed_buffer
耗时飙升,以及
Update RS
阶段时间占比异常升高(尤其在 Young GC 中)。启用以下参数后观察:
-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+UseStringDeduplication
(后者可减少部分写压力)
-XX:+UnlockDiagnosticVMOptions -XX:+LogVMOutput -Xlog:gc+ref=debug,gc+phases=debug,gc+ergo=trace
(JDK 10+) 重点检查日志中类似这样的行:
2026-04-14T06:45:22.112-0000: 128.456: [GC pause (G1 Evacuation Pause) (young), 0.0234567 secs] [Update RS (ms): Min: 8.2, Avg: 12.7, Max: 24.1, Diff: 15.9, Sum: 50.8]
若
Avg
> 10ms 或
Max
> 30ms,且伴随
Ext Root Scanning
时间同步上涨,基本可判定 RSet 更新已成瓶颈。 调整 Dirty Card Queue 相关参数缓解背压 不能盲目增大队列容量,需平衡内存开销与响应延迟。核心可控参数有三个:
-XX:G1ConcRefinementThreads
:并发 Refinement 线程数,默认为 CPU 核数的 1/4(向上取整)。突发写场景下建议设为
min(8, CPU核数/2)
,避免线程过多争抢 CPU;
-XX:G1RSetUpdatingPauseTimePercent
:控制 Young GC 中花在 RSet 更新上的时间占比上限,默认 10。若
Update RS
耗时经常超限,可临时调高至 15–20,但会压缩对象复制时间,可能增加 STW;
-XX:G1RSetScanBlockSize
:RSet 扫描块大小,默认 64。增大(如 128 或 256)可降低扫描次数,但单次扫描更重;减小则更细粒度,适合写模式高度分散的场景。 注意:
-XX:G1DirtyCardQueueMaxSize
(队列最大长度)极少需要手动调,JVM 会自动扩容;强行设大反而浪费堆外内存,且不解决根本问题。 写模式优化比调参更治本 很多团队花几小时调
G1RSetUpdatingPauseTimePercent
,却忽略代码层的写放大。典型高危模式包括: 循环内反复新建相同结构的对象(如
new HashMap()
),导致大量小对象跨 Region 分配; 使用
ArrayList
或
StringBuilder
未预估容量,频繁扩容 + 数组复制,触发大量跨 Region 引用; 将大对象(如 JSON 字符串、Protobuf 序列化结果)直接塞进缓存 Map,而它本身已超过 Region 一半大小,被归为 Humongous,其 RSet 维护成本极高。 真正有效的缓解手段是:对批量写路径做对象复用(ThreadLocal 缓存)、预分配集合容量、拆分超大对象为固定大小 chunk、或改用 off-heap 存储。RSet 是 G1 的基础设施,不是性能调节旋钮——它暴露的是写行为本身的结构性问题。

相关文章