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