一、 理论上的“卡顿”时刻:GC 周期中的 STW

每个 GC 周期的开始和结束,都有两个必须 Stop The World (STW) 的阶段。这是程序在理论上“绝对”会停顿的时刻。

  1. 标记准备阶段 (Sweep Termination)
  2. 标记终止阶段 (Mark Termination)

二、 实际中的“加剧”因素:为什么你会感觉到明显的卡顿?

在正常情况下,上述 STW 仅为微秒级。但当以下因素出现时,停顿时间会延长,或者即便没有 STW,你也会感觉到明显的性能下降(卡顿感)。

延长 STW 时间的因素

这类因素会导致程序完全停止运行,表现为极高的 P99 延迟,甚至程序瞬间“假死”。

  1. 抢占延迟 (Preemption Delay)
    • 原因:在进入 STW 前,GC 必须等待所有 Goroutine 停在安全点。如果某个 Goroutine 处于无法被抢占的状态(如紧凑循环、长时间 CGO 调用),会导致整个程序停下来等它一个。
  2. 根对象扫描负担 (Root Scanning)
    • 原因:STW 期间需要扫描栈、全局变量。
      • 海量 Goroutine:如果你有几十万个 Goroutine,即便每个栈扫描很快,累加起来的 STW 也会变得可观。
      • 巨大的全局变量:存储了大量指针的全局数据结构。
  3. 标记终止阶段的收尾工作 (Mark Termination)
    • 原因:在 STW 阶段需要完成最后的标记清理。如果并发阶段没能有效处理完复杂的指针引用,最后的收尾工作就会变重。

导致性能下降/响应变慢的因素

这类因素通常不会停止程序,但会显著消耗系统资源,导致业务逻辑变慢,表现为吞吐量下降或整体响应时间(Average/P90 Latency)拉长。

  1. GC 协助标记 (GC Assist / Mark Assist)
    • 机制:当分配内存过快时,Go 会扣除申请内存的协程的 CPU 时间,强迫它去帮 GC 干活。
    • 影响:这是最直接的“性能损耗”。用户请求的响应时间会因为被拉去干活而变长。
  2. 写屏障开销 (Write Barrier Overhead)
    • 机制:在标记期间,所有的指针赋值(如 a.ptr = b)都会触发额外的函数调用。
    • 影响:对于写密集的业务(如频繁更新缓存、修改复杂 Tree/Graph 结构),CPU 效率会下降。
  3. 后台标记 worker 抢占 CPU (Mark Workers)
    • 机制:GC 会默认占用约 25% 的 CPU 核心用于后台标记。
    • 影响:业务逻辑可用的 CPU 资源变少,在高负载情况下会加剧 CPU 竞争。
  4. 内存碎片与分配压力
    • 机制:由于 GC 无法进行内存整理(Non-moving),频繁的分配会导致碎片化。
    • 影响:这会导致 mallocgc 在分配内存时需要遍历更多的数据结构来寻找空闲空间,增加了分配本身的开销。

总结对比

类别表现形式用户感受核心指标
延长 STW程序完全静止尖锐的 P99 延迟,偶尔“假死”pause_ns
性能下降响应变慢,吞吐降低整体运行沉重,P50/P90 上升mark assist / CPU utilization