g1的copying算法通过复制存活对象到空region来避免内存碎片,但stw不可避免,因其需原子性更新引用和remembered set;stw时长取决于存活对象量、region数量及rset复杂度。

为什么 G1 的 Copying 算法能缓解堆碎片,但 STW 仍不可避
G1 在 Young GC 和 Mixed GC 阶段都使用复制算法(Copying),把存活对象从一组 Region 搬到另一组空闲 Region,搬完直接清空源 Region。这天然避免了 CMS 那种标记-清除后留下的小碎片——所以 G1 不会因“空间不够分配大对象”而频繁触发 Full GC。但关键点在于:**复制必须在 STW 下完成**,因为对象引用地址会变,运行中修改会导致指针悬空。哪怕只搬几千个 Region,只要涉及对象移动和卡表(Remembered Set)更新,JVM 就得暂停所有应用线程。
Copying 引发的 STW 时间主要取决于哪些参数
STW 时长不是由堆总大小决定的,而是由当前要复制的存活对象总量、Region 数量、以及 Remembered Set 的复杂度共同决定。重点关注以下三个配置项:
-
-XX:MaxGCPauseMillis:G1 的目标停顿时间(默认 200ms),它不保证上限,只是调度策略的参考值;设得太低会导致更频繁但更小的 GC,反而增加总体开销 -
-XX:G1HeapRegionSize:Region 越大,单个 Region 存活对象越多,复制压力越大;但太小又导致 Remembered Set 膨胀,影响并发标记阶段的扫描效率 -
-XX:G1MixedGCCountTarget和-XX:G1OldCSetRegionThresholdPercent:控制 Mixed GC 回收多少老年代 Region;回收太多,Copying 工作量陡增;回收太少,老年代碎片缓慢堆积,最终 Humongous 分配失败触发 Full GC
如何从 GC 日志里定位 Copying 阶段的 STW 瓶颈
看 GC log 中带 Copy 关键字的行,尤其是 Young GC 或 Mixed GC 的 STW 阶段输出。典型日志片段如下:
[GC pause (G1 Evacuation Pause) (young), 0.1234567 secs] [Eden: 1200M(1200M)->0B(1100M), Survivors: 100M->150M, Old: 2500M->2480M] [Times: user=0.45 sys=0.02, real=0.12 secs]
这里 real=0.12 secs 就是本次 Copying 的 STW 实际耗时。需要关注的信号包括:
- 如果
real显著大于user(比如 real=0.3s,user=0.08s),说明大量时间花在等待或同步上,可能是 Remembered Set 更新竞争激烈 - 若每次 Young GC 的 Eden 区回收后,Survivor 区增长异常快(如
Survivors: 50M->200M),说明对象晋升加速,老年代 Region 过早被填满,后续 Mixed GC 的 Copying 压力会集中爆发 - 出现
Humongous allocation failed后紧跟着 Full GC,说明 Copying 无法及时腾出连续 Region 给巨型对象,本质是 Copying 调度滞后于内存分配节奏
真正容易被忽略的 Copying 性能陷阱
很多人调优只盯着堆大小和停顿目标,却忽略了两个隐蔽但致命的点:
- 大对象不是“大了才危险”,而是**一旦超过 Region 大小的 50%(即触发 Humongous 标记)就开始影响 Copying 效率**——因为 Humongous Region 不参与常规复制,只在 Full GC 或 Mixed GC 的末期被整体回收,它们像钉子一样卡在堆里,阻碍 Region 整合
- Remembered Set 并非免费午餐:每个 Region 对应一个 RSet,记录跨 Region 的引用。RSet 越大,Copying 前的“根扫描”越慢,且 RSet 本身也要在 STW 阶段校验和更新——这意味着即使存活对象少,RSet 脏数据多也会拉长 STW
所以观察 GC 日志时,不能只数 Copy 耗时,还要看 [RSet updating] 和 [Ref Proc] 的耗时占比。这两个环节常被淹没在总时间里,却是优化 Copying STW 最实际的切口。










