在高性能后端开发中,垃圾回收(GC)的停顿时间(STW)往往是系统响应抖动的元凶。Go 语言通过不断演进,在 1.8 版本引入了混合写屏障(Hybrid Write Barrier),极大地缩短了 STW 耗时。本文将带你透视这一机制的核心原理,并通过工具实操观察其表现。
一、三色标记法的基本原理
Go 的 GC 采用的是三色标记法。它将堆上的对象分为三类:
– 白色:潜在的垃圾,其内存可能会被回收。
– 灰色:活跃对象,但其引用的对象尚未被扫描。
– 黑色:活跃对象,且其引用的所有对象都已被扫描完毕。
标记过程:
1. 起始时所有对象都是白色。
2. 从根对象(栈、全局变量)开始扫描,将其标记为灰色。
3. 遍历灰色对象,将它们引用的对象标记为灰色,然后将自己标记为黑色。
4. 重复上述步骤,直到没有灰色对象。
5. 回收所有剩余的白色对象。
二、为什么要写屏障?
在并发标记过程中,如果用户逻辑(Mutator)修改了指针,可能会发生“漏标”:
1. 一个黑色对象指向了一个白色对象。
2. 灰色对象原本指向这个白色对象的引用被断开了。
这会导致该白色对象被错误回收。为了解决这个问题,Go 引入了写屏障(Write Barrier)。
三、从插入/删除屏障到“混合写屏障”
早期的 Go 版本尝试过插入写屏障或删除写屏障,但它们各有缺陷(比如需要在结束标记时重新 STW 扫描栈)。Go 1.8 引入的混合写屏障结合了二者优点:
- GC 开始将栈上的对象全部扫描并黑色标记(之后不再二次扫描)。
- GC 期间,任何在栈上创建的新对象直接标记为黑色。
- 堆上被删除的对象标记为灰色。
- 堆上新添加的对象标记为灰色。
通过这种机制,保证了黑色对象引用的白色对象如果发生转移,一定会被记录并置灰,从而确保安全性并消除了对栈的重复扫描,使 STW 降至毫秒级甚至微秒级。
四、实操:如何观察 GC 行为?
我们可以通过设置环境变量 GODEBUG 来实时查看 Go 程序的 GC 情况。下面是一个简单的示例:
package main
import (
"fmt"
"runtime"
"time"
)
func main() {
// 持续申请内存,模拟垃圾回收压力
go func() {
for {
_ = make([]byte, 10*1024*1024) // 申请 10MB
time.Sleep(time.Millisecond * 100)
}
}()
for {
fmt.Println("Running...")
time.Sleep(time.Second)
}
}
运行命令:
GODEBUG=gctrace=1 go run main.go
输出解析:
你会看到类似 gc 1 @0.001s 2%: … 0.050+0.42+0.002 ms clock … 的输出。
– 0.050 ms:第一次 STW 时间(标记开始)。
– 0.42 ms:并发标记时间(此时程序正常运行)。
– 0.002 ms:第二次 STW 时间(标记终止)。
从数据可以看出,实际导致停顿的 STW 时间极短,这正是三色标记与混合写屏障协同工作的成果。
五、总结
Go 垃圾回收的进化史就是一部对抗 STW 的斗争史。理解三色标记的“染色”逻辑以及混合写屏障如何通过“着色补偿”保护对象,能帮助我们更好地编写高性能并发程序,并在系统调优时做到心中有数。
汤不热吧