Go 内存回收机制 | 青训营笔记

197 阅读3分钟

Go GC工作流程 Golang GC的大部分处理是和用户代码并行的

1Mark: 包含两部分:
aMark Prepare: 初始化GC任务,包括开启写屏障(write barrier)和辅助GC(mutator assist),统计root对象的任务数量等。这个过程需要STW
bGC Drains: 扫描所有root对象,包括全局指针和goroutine(G)栈上的指针(扫描对应G栈时需停止该G),将其加入标记队列(灰色队列),并循环处理灰色队列的对象,直到灰色队列为空。该过程后台并行执行
2Mark Termination: 完成标记工作,重新扫描(re-scan)全局指针和栈。因为Mark和用户程序是并行的,所以在Mark过程中可能会有新的对象分配和指针赋值,这个时候就需要通过写屏障(write barrier)记录下来,re-scan 再检查一下。这个过程也是会STW的。
3Sweep: 按照标记结果回收所有的白色对象,该过程后台并行执行
4Sweep Termination: 对未清扫的span进行清扫, 只有上一轮的GC的清扫工作完成才可以开始新一轮的GC

写屏障(Write Barrier) 因为go支持并行GC, GC的扫描和go代码可以同时运行, 这样带来的问题是GC扫描的过程中go代码有可能改变了对象的依赖树。

例如开始扫描时发现根对象A和B, B拥有C的指针。

GC先扫描A,A放入黑色 B把C的指针交给A GC再扫描B,B放入黑色 C在白色,会回收;但是A其实引用了C。 为了避免这个问题, go在GC的标记阶段会启用写屏障(Write Barrier).

启用了写屏障(Write Barrier)后,在GC第三轮rescan阶段,根据写屏障标记将C放入灰色,防止C丢失。

辅助GC 如果GC回收的速度跟不上用户代码分配对象的速度呢? Go 语⾔如果发现扫描后回收的速度跟不上分配的速度它依然会把⽤户逻辑暂停,⽤户逻辑暂停了以后也就意味着不会有新的对象出现,同时会把⽤户线程抢过来加⼊到垃圾回收⾥⾯加快垃圾回收的速度。这样⼀来原来的并发还是变成了STW,还是得把⽤户线程暂停掉,要不然扫描和回收没完没了了停不下来,因为新分配对象⽐回收快,所以这种东⻄叫做辅助回收。

垃圾回收触发机制

  1. 内存分配量达到阀值触发GC 每次内存分配时都会检查当前内存分配量是否已达到阀值,如果达到阀值则立即启动GC。 阀值 = 上次GC内存分配量 * 内存增长率 内存增长率由环境变量GOGC控制,默认为100,即每当内存扩大一倍时启动GC。
  2. 定期触发GC 默认情况下,最长2分钟触发一次GC,这个间隔在src/runtime/proc.go:forcegcperiod变量中被声明:
// forcegcperiod is the maximum time in nanoseconds between garbage
// collections. If we go this long without a garbage collection, one
// is forced to run.
//
// This is a variable for testing purposes. It normally doesn't change.
var forcegcperiod int64 = 2 \* 60 \* 1e9
  1. 手动触发 程序代码中也可以使用runtime.GC()来手动触发GC。这主要用于GC性能测试和统计。