根节点枚举必须暂停用户线程以确保一致性,因栈帧等GC Roots可能被实时修改,JVM需在所有线程到达安全点后冻结执行,获取可靠快照,故即使ZGC/G1等也存在亚毫秒级STW。

根节点枚举必须冻结线程栈
GC 启动时第一步是确定哪些对象“活”,而起点只能是 GC Roots:比如 Java 线程栈帧里的局部变量、静态字段、JNI 引用等。这些引用随时可能被用户线程修改——例如方法刚把一个对象赋给局部变量,下一行就置为 null。如果不暂停线程,扫描栈时拿到的引用快照就不可靠,要么漏标(该活的对象被回收),要么错标(已死对象残留)。所以 JVM 必须在所有线程都到达 安全点 后统一停顿,才能读取一致的栈状态。
G1/ZGC/Shenandoah 的 STW 并没消失,只是压缩到亚毫秒
很多人误以为 ZGC 或 Shenandoah “没有 STW”,其实只是把原本耗时的操作尽量并发化,但仍有极短的强制停顿:
-
初始标记阶段仍需 STW 扫描根集合(哪怕只花 0.05ms) -
最终标记(G1 的Remark)或重映射(ZGC 的Relocate)阶段,仍要短暂 STW 更新部分关键指针 - Shenandoah 在 JDK 17 才实现
并发线程堆栈处理,此前堆栈扫描本身就会导致几毫秒到几十毫秒停顿(尤其在线程数多、栈深的应用里)
对象移动时更新引用必须排他
复制算法(如 G1 的 Evacuation)或整理算法(如 Serial 的 Compact)会把对象从旧地址挪到新地址。此时若用户线程正在读写该对象的引用字段,就可能读到迁移中无效地址,或写入已被覆盖的内存。JVM 不可能给每个字段访问都加屏障(性能崩盘),所以必须 STW,在停顿窗口内批量更新所有栈/寄存器/堆中指向该对象的引用。这也是为什么 CMS 被淘汰——它不移动对象,但换来的是内存碎片和更难控的回收失败风险。
VMThread 协作机制决定了协调成本无法绕开
Java 线程触发内存分配失败后,并不自己执行 GC,而是提交一个 VM_Operation 给全局唯一的 VMThread。这个线程负责串行执行所有需要全局一致性的操作,包括 GC、类卸载、锁膨胀等。所有 Java 线程必须等待 VMThread 完成当前任务才能继续——这本身就是一种隐式 STW 的调度约束。即使 GC 主体并发了,VMThread 的排队、状态同步、安全点检查这些环节仍构成不可省略的协调开销。
G1Remark 停顿飙升,大概率是并发标记期间用户线程修改太多引用;而 ZGC 出现 2ms 以上停顿,往往意味着 Load Barrier 触发了非预期的 slow path,或者元空间回收被拖进来了。


















