简介

Go的GC主要针对堆内存,而不是栈内存。因为:

  • 栈内存自动分配和释放,遵循先进后出原则。
  • 函数调用信息和局部变量会根据栈指针变化被自动清理。

Go主要通过三色标记法来标记和扫描所有堆对象,然后通过混合写屏障来保证对象不丢失,并减少STW(stop the world,即暂停程序)时间。

三色标记法

工作流程

GC维护三个集合:白色标记集合、黑色标记集合、灰色标记集合。首先把所有对象标记为白色,全部加入白色标记集合,然后执行下面的操作进行标记:

  • 扫描根对象,得到根对象引用的对象,标记为灰色,全部加入灰色集合。遍历后的根对象加入黑色集合。
  • 遍历灰色标记集合,将可达对象标记为灰色,遍历后的灰色对象标记为黑色,分别加入灰色标记集合和黑色标记集合。
  • 重复上一步,只到灰色标记集合没有任何对象。
  • 收集所有白色对象,即垃圾。

这里的根对象主要是指:全局变量、栈上的变量、寄存器中的变量等。

需要STW吗?

三色标记法如果不使用STW,会出现对象丢失的问题。例如:

  • 一个白色对象没有任何被引用,但是在标记时,被黑色对象引用了。由于不会重复扫描这个黑色节点的引用,所以这个白色对象会被判断为垃圾。
  • 如果一个新的对象被创建,标记为白色,最终会被当作垃圾。

而三色标记法如果在执行时一直使用STW,会时程序出现明显的卡顿。因此有两个解决方法

  • 强三色不变式:强制黑色对象不能引用白色对象。
  • 弱三色不变式:允许黑色对象引用白色对象,但是需要这个白色对象上游有灰色对象。

这两个方法需要通过屏障机制实现。

混合写屏障

混合写屏障由插入屏障和删除屏障组成。

插入屏障

当黑色对象引用白色对象时,将白色对象标记为灰色,为了性能Go仅在堆上插入写屏障,栈上不启用。缺点是在垃圾回收完成前,需要重新STW,并重新扫描整个栈空间,保证没有遗漏的引用关系。传统的Dijkstra插入屏障有一个问题:栈上不应用写屏障(为了性能考虑,不然每次栈操作都要应用写屏障),这可能导致以下情况:

  • 栈上对象引用了一个白色对象
  • 这个引用关系被移除
  • 没有写屏障保护,这个白色对象可能被错误回收

所以在使用插入屏障时,必须在GC结束前重新STW并扫描整个栈,确保没有遗漏的引用。

删除屏障

某个引用删除时,如果被引用对象是白色或者灰色,则将其标记为灰色。满足弱三色不变式,将主动可能丢失的对象提升为灰色。缺点是回收精度低,某些实际不可达对象,要下一轮GC才能回收。

混合写屏障

GC开始:栈上所有元素都标记为黑色 GC期间:

  • 任何新创建对象都标记为黑色。(栈上之前的对象和新对象都是黑色,不会被错误清除)
  • 被删除引用的对象标记为灰色。
  • 被添加引用的对象标记为灰色。

垃圾回收的时机

Go的GC触发时机由多种因素决定。常见的有

基于内存分配阈值触发

环境变量GOGC控制GC触发频率,默认值为100。默认GOGC=100意味着当内存使用量达到上次GC后存活内存的2倍时触发新的GC。

基于时间触发

如果超过一定时间,约2分钟没有执行GC,会触发强制GC,保证即使在内存使用较少的情况下,GC也会定期执行。

手动触发

调用runtime.GC()可以手动触发。

import "runtime"
func f(){
    runtime.GC()
}

系统内存压力触发

通过GOMEMLIMIT环境变量可以设置Go程序最大内存使用量。当系统资源紧张,可能触发提前GC。

标记辅助触发

分配内存速度太快,GC跟不上分配速度时触发的GC。