提问:请简述 Go 语言的垃圾回收器(Garbage Collector, GC)?
1 | 标记阶段
该阶段对以 span-object 为载体的内存进行扫描。由于 GC 与 Go 语言主程序一起并发执行,所以须要在扫描时监控内存可能出现的状态改变。
写屏障(Write Barrier)算法便应运而生,启动写屏障的唯一途径就是短暂停止 Go 语言主程序,称之为 STW(Stop the World),并以 Start the World 恢复:
在 GC 工作开始时,Go 语言随即为每个处理机 P 配置一个 mark worker
线程 G 来帮助标记内存,它们负责为进入 STW 后的清理做前期工作。
一旦根程序(Root)被加入待处理队列 work pool
,标记周期便正式开始,这些线程 G 开始遍历 span 并标记内存块 object。
以程序 main.go
为例,以下的配图均服务于本实例的三个变量s1
,s2
和_
:
type struct1 struct {
a, b int64
c, d float64
e *struct2
} // 40 bytes
type struct2 struct {
f, g int64
h, i float64
} // 32 bytes
// go:noinline
func allocStruct1() *struct1 {
return &struct1{
e: allocStruct2(),
}
}
// go:noinline
func allocStruct2() *struct2 {
return &struct2{}
}
func main() {
_ = trace.Start(os.Stderr)
defer trace.Stop()
s1 := allocStruct1()
s2 := allocStruct2()
func() {
_ = allocStruct2()
}()
runtime.GC()
fmt.Printf("s1 = %X, s2 = %X\n", &s1, &s2)
}
因为结构体 struct1
和 struct2
大小分别为 40 和 32 个字节,被分配到 48 和 32 字节大小的 span,后者不包含指针,所以它被存储在非指针的 span 区:
以是否包含指针区分 span,使得 GC 在扫描过程避免不必要的性能开销,这也是 GC 高并发性能彪悍的关键原因。
每当主程序 main
的内存分配完成,GC 的 mark worker
线程便会强制执行一轮的扫描,流程如下:
GC 从栈映射 span 的栈(stack)开始,根据扫描到的指针引导的地址来递归式访问 span,span 一旦被标记为 no span
,后续不会继续被扫描,递归便结束;反之继续根据指针递进。
s1
包含了一个 4 字节大小的指针,指向了 32 字节大小的 span,而 s2
不包含指针,便是递归的尽头。
由于不同的 Goroutine 根据在队列中的指针并发完成,之前入队的background mark worker
会出队,扫描 span 中的 object 然后将找到的指针加入队列:
2 | 着色过程
着色线程 mark worker
利用三色标记算法
来完成任务:
- 起初所有的 object 都被认定为
白色
; - 比如栈、堆和全局变量这种 object 被标记为
灰色
。
GC 的这些线程在上述步骤的基础上:
- 将灰色 object 标记为
黑色
; - 将灰色 object 所包含的所有指针所指向的地址都标记为
灰色
。
递归执行以上两个步骤,最终对象非黑即白,其中白色即未被引用且可以被回收的 object。
以上述程序为例,起初所有 object 被视作白色,遍历后将可达者标记为灰色,如果 object 处标记为 no sacan
,则视为递归的尽头,被标记成黑色:
灰色的 object 被加入待扫描队列,出队被扫描后即被标记为黑色:
重复执行上述步骤,直至没有待处理的灰色 object:
最终,黑色 object 即为被使用,而白色为可回收。
上述实例中,结构体 struct2
的实例被匿名函数创建被分配至上图的 span class 3
,透过汇编源码得知,在栈上不可达:
TEXT "".main.func1(SB), ABIInternal, $24-0
MOVQ TLS, CX
PCDATA $0, $-2
MOVQ (CX)(TLS*2), CX
PCDATA $0, $-1
CMPQ SP, 16(CX)
PCDATA $0, $-2
JLS 64
PCDATA $0, $-1
SUBQ $24, SP
MOVQ BP, 16(SP)
LEAQ 16(SP), BP
FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
NOP
LEAQ type."".struct2(SB), AX
故 struct2
的实例 _ 由始至终为白色,必将被 GC 回收,实践上使用匿名变量能有效减少内存开销。
mspan
数据结构中的 gcmarkBits *gcBits
用于实现 span 的颜色标记:
灰色和黑色在 gcmarkBits
中皆为 1,不同之处在于黑色为颜色标记的终点,灰色为待续。
GC 使 Go 程序短暂停止,将对每个写屏障所做的更改更新至 work pool
直至清空队列。
3 | 小结
使用顶配 Intel i-7 四核心处理机和标准库 runtime/trace
可视化分析上述的垃圾回收线程,在命令栏依次执行:
go run main.go 2> trace.out
go tool trace trace.out
该命令会多线程执行的序列化结果呈现于浏览器:
GC 为上图顶部淡蓝进度条,随主程序启动后执行;
Proc 0 为标准库 runtime/trace
的监视线程 G19
,用作本次资源监控,可以忽略;
Proc 1 为上述 Go 语言测试实例的本身主线程 runtime.main
;
Proc 1 粉蓝色线程 G2
即为 runtime.gcBgMarkWorker
;
Proc 0 粉红色前边短暂的绿色为 Stop the World 线程开始处,即 sweep termination
;
Proc 0/1/4 的粉红色 GC (dedicated) 为 marking worker
线程,就是递归遍历 span 的着色操作;
Proc 4 肉粉色 STW 为线程上述 Stop the World 的短暂停顿结束,最后由箭头交棒给主线程 G1
。