之前自以为对Go语言的GC了如指掌,结果昨天被面试官疯狂拷打。于是有了这篇博客。
Go语言的GC主要有三个版本:
- v1.3 标记清除法
- v1.5 三色标记法 + 写屏障
- v1.8 混合写屏障
v1.3标记清除法
Go1.3的标记清除法是Go语言早期版本的垃圾收集策略。这种方法在垃圾收集过程中需要暂停整个程序(Stop-the-World,STW),这可能对程序的性能产生负面影响。标记清除法可以在大体上分为四个步骤:
- STW,暂停运行中的程序
- 从
根节点
出发,标记出所有可达对象 - 清除所有未被标记的对象
- 程序恢复运行
缺点:
- 执行期间需要把整个程序完全暂停,不能异步的进行垃圾回收。
- 标记阶段需要扫描整个堆内存以找出所有可达对象,这可能会消耗大量时间和计算资源。
- 容易产生大量不连续的内存碎片,碎片太多可能会导致后续没有足够的连续内存分配给较大的对象,从而提前触发新的一次垃圾收集动作。
v1.5三色标记法+写屏障
Go1.5版本的GC采用的是三色标记法+写屏障。
三色标记法
Go 三色标记法是一种并发的垃圾回收算法。
三色标记法将对象分为三类:
- 白色对象:未被标记的对象
- 灰色对象:正在被标记的对象
- 黑色对象:已经被标记的对象
流程如下:
- 初始时,所有的对象为白色。
- 从
根对象
开始,将能够直达的对象标记为灰色,放入灰色队列中。 - 从灰色队列中取出一个灰色对象,将其引用的对象放入灰色队列。将该灰色对象标记为黑色,并移除灰色队列。
- 重复上面的过程,直至灰色队列为空。
- 清除所有白色对象。
在没有用户态代码并发修改三色关系的情况下,回收可以正常结束。
假设某个灰色对象A指向白色对象B,此时并发的用户态代码将黑色对象C指向了白色对象B,并将灰色对象A对白色对象B的引用移除,则在继续扫描的过程中,白色对象B永远不会被标记为黑色对象。进而对象B被错误回收。
为了解决这种问题,引入了写屏障。
三色不变式
三色不变式是确保三色标记法正确的关键条件。它要求在整个标记过程中,以下两个条件始终成立:
- 强三色不变式:不允许黑色对象直接应用白色对象。
- 弱三色不变式:所有被黑色对象引用的白色对象都必须通过灰色对象可达。
写屏障
当一个对象引用另外一个对象时,将另外一个对象标记为灰色,以此满足强三色不变式,不会存在黑色对象引用白色对象。
v1.8三色标记法+混合写屏障
在Go1.8 GC策略中,进一步将写屏障优化为了混合写屏障。
混合写屏障
混合写屏障分为:插入写屏障和删除写屏障
- 插入写屏障:当一个黑色对象引用了一个白色对象时,插入写屏障会被触发,此时垃圾收集器会将该白色对象被标记为灰色对象。
- 删除写屏障:当一个对象的引用被删除时,删除写屏障会被触发。此时垃圾收集器会将该对象标记为灰色。
根对象是个啥
Go语言的GC根对象主要包括全局变量、执行栈、寄存器中的指针以及其他运行时数据结构中的指针。
- 全局变量:全局变量是在程序整个生命周期中都可以访问的变量。由于它们在程序开始时就已经存在,因此被视为根对象的一部分。
- 执行栈:每个Goroutine 都有自己的执行栈。执行栈上包含的函数局部变量以及指向堆上分配的对象的指针都会被视为根对象。
- 缓存器:在某些情况下,寄存器的值可能表示一个指针。这些指针可能指向某些由Go程序分配的堆内存区块。虽然寄存器的访问通常是与CPU架构和具体实现紧密相关的,但GC在特定的情况下也会将寄存器中的指针作为根对象来扫描。
- 其他运行时数据结构:Go语言的运行时系统维护了一些其他的数据结构,如mcache、span等。这些数据结构中的某些部分也可能包含指向堆内存区块的指针,因此它们也可能被视为根对象。
GC回收触发时机
内存分配达到阀值触发
每次内存分配时都会检查当前内存分配量是否已达到阀值,如果达到阀值则立即启动GC。
阀值 = 上次GC内存分配量 * 内存增长率
内存增长率由环境变量GOGC控制,默认为100,即当内存扩大一倍时启动GC。
定期触发GC
默认情况下,最长2分钟触发一次GC,这个间隔在src/runtime/proc.go:forcegcperiod变量中被声明:
go
// 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
手动触发
程序中可以通过 runtime.GC()
来手动触发GC