STW不只发生在GC时,还可能由偏向锁撤销、类卸载等触发,但90%以上长/频停顿源于GC;需通过-XX:+PrintGCApplicationStoppedTime和安全点统计定位根因,G1/ZGC等新GC仍有不可省略的STW环节。

STW不是GC专属,但GC是最常见诱因
Stop-The-World(STW)本质是JVM强制暂停所有应用线程的瞬间,**不只发生在GC时**——比如偏向锁批量撤销、类卸载、JIT反优化、甚至某些Unsafe内存操作失败,都可能触发。但现实中90%以上的长停顿或高频停顿,根源在GC流程中的关键阶段。
真正要盯的不是“有没有STW”,而是“为什么停这么久/这么频繁”。GC日志里GC remark耗时0.8秒,和vmop: BulkRevokeBias停了0.7秒,解决路径完全不同。
- 先确认是不是GC导致:加
-XX:+PrintGCApplicationStoppedTime,看停顿时长是否与GC日志时间戳对齐 - 不对齐?大概率是安全点阻塞,立刻补上
-XX:+PrintSafepointStatistics -XX:PrintSafepointStatisticsCount=1 - 注意:CMS的
remark、G1的final marking、ZGC的Relocate Start等阶段,都属于“必须STW”的硬性环节,无法跳过
看懂GC日志里的STW耗时拆解
光看GC remark 0.678 sec没用,得拆开它内部干了什么。开启-XX:+PrintReferenceGC和-XX:+PrintAdaptiveSizePolicy后,典型CMS重新标记日志长这样:
2025-04-05T10:00:00.123+0800: 123.456: [GC remark 123.457: [Finalize Marking 0.234 sec] [GC Ref Proc 0.123 sec] [Unloading 0.045 sec], 0.678 sec]
这里三段耗时说明不同问题:
-
Finalize Marking:扫描Card Table + 修正并发标记漏标对象 → 对象引用链越深、跨代引用越多,这步越慢 -
GC Ref Proc:处理软引用、弱引用、虚引用 → 若代码大量使用WeakHashMap且key未及时回收,会卡在这 -
Unloading:卸载无用类 → 动态生成类多(如Groovy脚本、某些ORM)、或Spring热部署残留类,容易拖慢
安全点阻塞:没有GC日志却频繁停顿的元凶
现象:jstat -gc显示GC次数极少,但-XX:+PrintGCApplicationStoppedTime打印出一堆Total time for which application threads were stopped: 0.012xxx seconds,且无对应GC事件。
这是典型的“安全点等待”——JVM在等所有线程跑到最近的安全点再挂起。常见触发点:
-
vmop: BulkRevokeBias:大量线程竞争同一把偏向锁,JVM决定批量撤销 → 关键参数-XX:-UseBiasedLocking可直接禁用 -
vmop: RevokeBias:单个对象偏向锁撤销 → 通常影响小,但若高频创建/销毁同一类对象,也值得查 -
vmop: ThreadDump或vmop: Deoptimize:JMX线程dump、或JIT编译器发现热点方法需退优化 → 检查是否有人定时调用Thread.getAllStackTraces()
别信“只是几毫秒”——累计几十次,用户就明显感到卡顿。
G1/ZGC/Shenandoah的STW陷阱:不是用了新GC就万事大吉
很多人以为切到ZGC或Shenandoah就能消灭STW,其实它们只是把大部分工作并发化,**仍有不可省略的STW环节**:
- ZGC:每次GC都有
Initial Mark和Remark两个STW阶段,虽短(通常100GB)或对象图极复杂,Remark仍可能飙到10ms+ - G1:
Mixed GC中Evacuation本身不STW,但Remembered Set更新、Root Region Scanning这些前置步骤仍需短暂STW - 共性坑:大对象(
Humongous Object)在G1/ZGC中不进常规Region,而是独占多个Region,回收时容易引发额外STW;检查日志里是否有humongous allocation警告
最常被忽略的一点:JVM参数冲突。比如给ZGC配了-XX:+UseG1GC(无效),或G1启用了-XX:+UseStringDeduplication却没开-XX:+UseG1GC,反而让STW更不可控。










