Go 1.20+中,STW实际耗时最准方式是用go tool trace:运行go run -gcflags="-gcdebug=2" main.go 2> trace.out后执行go tool trace trace.out,在Web界面搜索GC并展开横条,查看“STW start”到“STW done”区间长度;gctrace=1日志中第一个毫秒值(如0.012)仅为初始STW近似值,不含栈重扫描等微小停顿。

怎么看当前程序的STW实际耗时
Go 1.20+ 默认开启详细GC追踪,不需要额外编译参数。最直接的方式是用 go tool trace 抓取运行时事件:
- 启动程序时加
GODEBUG=gctrace=1,终端会实时打印类似gc 1 @0.123s 0%: 0.012+0.45+0.008 ms clock, 0.048+0.12/0.33/0.048+0.032 ms cpu, 4->4->2 MB, 8 MB goal, 4 P的日志,其中第一个数字(如0.012)就是 STW 时间(单位 ms) - 更精确的分析要靠 trace:运行
go run -gcflags="-gcdebug=2" main.go 2> trace.out(注意是-gcdebug=2,不是-gcflags="-trace"),然后执行go tool trace trace.out - 在 Web 界面中点「View trace」→ 搜索
GC→ 找到每轮 GC 的横条,展开后看STW start到STW done的区间长度,这才是真实暂停时间
注意:gctrace=1 输出的 STW 是近似值,可能漏掉栈重扫描等微小停顿;go tool trace 能捕获全部 STW 阶段,包括标记终止(mark termination)和清除前的最终同步。
为什么 trace 里看到的 STW 比 gctrace 日志长
因为 Go 的 STW 不是单次大暂停,而是分阶段发生的:
- 初始 STW(
gcStart):仅扫描根对象(goroutine 栈、全局变量),通常 - 标记终止 STW(
mark termination):重新扫描所有 Goroutine 栈,确保没有漏标,这是最常被忽略的“第二段 STW”,耗时可能占整轮 STW 的 70% 以上 - 清除前同步 STW(极短,常被合并进上一阶段)
gctrace=1 只统计第一段,而 go tool trace 会把所有 STW 阶段串起来算总长。线上服务如果 TP99 延迟突增且与 GC 时间吻合,大概率是标记终止阶段拖长了——这说明栈上有大量活跃 goroutine 或栈帧过大。
立即学习“go语言免费学习笔记(深入)”;
怎么确认是不是 STW 导致请求延迟抖动
不能只看平均 STW,得关联业务指标:
- 用
pprof同时抓取runtime/trace和 HTTP handler 的耗时分布,对比 GC 时间点与 p99 延迟峰值是否重叠 - 检查
runtime.ReadMemStats中的PauseNs字段数组,它记录最近 256 次 GC 的每次 STW 纳秒级耗时,可导出做直方图:超过 500μs 的比例高,就说明有风险 - 设置
GODEBUG=gcpacertrace=1,观察 GC 是否频繁触发(比如每秒多次),高频 GC 会让 STW 叠加效应更明显
特别注意:STW 本身不消耗 CPU,但会阻塞网络 I/O 和定时器,所以即使 STW 只有 200μs,若恰逢一个关键 HTTP 请求正在写响应体,就会导致该请求整体延迟跳变。
GOGC 调低真能减少 STW 吗
不一定,甚至可能适得其反:
- 设
GOGC=20会让 GC 更频繁,单次堆扫描量变小,初始 STW 缩短,但标记终止阶段仍需扫全量栈,这部分几乎不受堆大小影响 - 高频 GC 会增加写屏障开销,间接抬高用户代码延迟,还可能引发 mutator assist(用户 goroutine 协助标记),反而让 CPU 更忙
- 真正压低 STW 的关键是减少栈扫描压力:避免深递归、控制 goroutine 栈大小(用
runtime/debug.SetMaxStack)、及时关闭无用 channel 和 timer
线上调优时,优先看 go tool trace 里 STW 分布是否集中在标记终止阶段——如果是,调 GOGC 没用,得去查 goroutine 泄漏或栈膨胀问题。



















