关于 Golang GC 和内存管理相关的流程和原理的一些总结。

GC 流程

golang GC 采用基于标记-清除的三色标记法,下图为 golang 一轮完整的 GC 的过程:

一轮完整的 GC,总是从 Off,如果不是 Off 状态,则代表上一轮GC还未完成,如果这时修改指针的值,是直接修改的。

Stack scan: 收集根对象(全局变量和 goroutine 栈上的变量),该阶段会开启写屏障(Write Barrier)。

Mark: 标记对象,直到标记完所有根对象和根对象可达对象。此时写屏障会记录所有指针的更改(通过 mutator)。

Mark Termination: 重新扫描部分全局变量和发生更改的栈变量,完成标记,该阶段会STW(Stop The World),也是 gc 时造成 go 程序停顿的主要阶段。

Sweep: 并发的清除未标记的对象。

三色标记

以上 Mark 阶段,采用的是三色标记法,是传统标记-清除算法的一种优化,主要思想是增加了一种中间状态,即灰色对象,以减少 STW 时间。
三色标记将对象分为黑色、白色、灰色三种:
- 黑色:已标记的对象,表示对象是根对象可达的。
- 白色:未标记对象,gc开始时所有对象为白色,当gc结束时,如果仍为白色,说明对象不可达,在 sweep 阶段会被清除。
- 灰色:被黑色对象引用到的对象,但其引用的自对象还未被扫描,灰色为标记过程的中间状态,当灰色对象全部被标记完成代表本次标记阶段结束。

三色标记的主要过程即:
1. 开始时所有对象为白色
2. 将所有根对象标记为灰色,放入队列
3. 遍历灰色对象,将其标记为黑色,并将他们引用的对象标记为灰色,放入队列
4. 重复步骤 3 持续遍历灰色对象,直至队列为空
5. 此时只剩下黑色对象和白色对象,白色对象即为下一步需要清除的对象

STW

传统的标记-清除算法,为了防止在标记过程中,对象引用发生变化,导致清除仍在使用的对象,需要 STW(Stop The World),这会造成程序的停顿。在三色标记的过程中,由于引入了灰色对象这一中间状态,标记过程和用户的 golang 代码中可以并发执行,不需要 STW,这极大的减少了应用的停顿时间。
三色标记具体如何避免在标记过程中对象应用的改变呢,这里用到了写屏障(Write Barrier)。

写屏障

在 GC 的流程中,Stack scan 这一步骤,启用了写屏障。写屏障的主要思想,是在标记的过程中,通过写屏障记录发生变化的指针,然后在 Mark termination 的 rescan 过程中,重新进行扫描,因为在这一步骤会 STW,所以在这一步骤完成后的白色对象,不会再被引用,可以直接清除。关于写屏障具体原理和实现,这里不再展开。

GC触发

golang 程序的执行过程中,如下几种情况下会触发 GC:
- 主动触发,用户代码中调用 runtime.GC 会主动触发 GC
- 默认每 2min 未产生 GC 时,golang 的守护协程 sysmon 会强制触发 GC
- 当 go 程序分配的内存增长超过阈值时,会触发 GC

内存分配

golang 内存分配分为堆内存和栈内存。
栈:一般函数内部执行中声明的变量,函数返回直接释放,不会引起垃圾回收,对性能无影响。
堆:有引用到的内存空间,靠 GC 回收,会影响程序进程。

内存逃逸

逃逸分析是指由编译器决定内存分配的位置,不需要程序员指定。即由编译器决定新申请的对象会分配到堆上还是栈上。
逃逸分析场景:
1. 指针逃逸
go 将函数内定义的变量返回到函数外,会将本应分配到栈上的内存分配到堆上。
2. 栈空间不足逃逸
当栈空间不足或无法判断当前切片长度时会将对象分配到堆上。
3. 动态类型逃逸
当函数参数为 interface 类型,编译期间无法确定参数的具体类型,也可能会产生逃逸。

参考列表