Golang垃圾回收机制

Golang垃圾回收机制:标记清理法、三色标记法、混合写屏障。


垃圾回收(Garbage Collection)是编程语言提供的自动内存管理机制,自动释放不需要的对象,让出内存资源。

Golang的垃圾回收机制经历了很多次变革:

  • Go V1.3 标记-清除(mark and sweep)算法;
  • Go V1.5 三色标记法,STW;
  • Go V1.5 屏障机制,强弱三色不变式,插入屏障、删除屏障;
  • Go V1.8 混合写屏障。

一、标记-清除算法

标记清除算法分为两个阶段,第一个阶段是标记(mark phase),第二个阶段是清除阶段(sweep phase)。具体步骤是:

  1. 暂停业务逻辑(Stop The World,STW),分类出可达和不可达的对象,做上标记。如果有其他对象或者程序使用的对象,则为可达的,没有其他对象使用的则是不可达的。
  2. 从程序入口开始,依次将可达对象做上标记。
  3. 清除没有标记的对象,停止暂停,让程序继续跑。

执行标记清除算法的时候,需要执行STW,程序暂停,CPU不执行用户代码,全部用于垃圾回收,很耗时,而且程序会出现卡顿。

算法过程干脆,但是有缺点:

  • STW让程序暂停,出现卡顿(重要问题);
  • 标记需要扫描整个heap;
  • 清除数据会产生heap碎片。

V1.3之前,STW的暂停范围:启动STW -> 标记 -> 清除 -> 停止STW,全部的GC时间都在STW的范围之内。V1.3的时候做了一个优化,STW的暂停范围:STW -> 标记 -> 停止STW。清除在停止STW之后,因为要清除的对象已经是不可达对象了,所以不会出现回收写冲突的问题。

但是无论怎么优化,标记清除算法都会暂停整个程序。

二、三色标记法

三色标记法就是使用三个阶段来标记对象,具体过程:

  1. 程序起初创建,全部标记为【白色】,每次新创建的对象,默认颜色都是【白色】。(程序有一个根节点集合,每个根节点都有指向不同的对象。)
  2. 从根节点开始遍历所有的对象,遍历到的对象从【白色】变为【灰色】。遍历不是递归的形式,只遍历一层。第一次遍历是根节点的可达对象。
  3. 第二次就把当前的灰色节点的可达对象从【白色】放入【灰色】,然后把当前【灰色】放入【黑色】。然后下一次又重复操作,知道所有【灰色】没有任何对象。
  4. 回收所有【白色】对象,也就是回收垃圾。剩下的全部都是【黑色】对象。

这里面可能会有很多并发流程均会被扫描,执行并发流程的内存可能相互依赖,为了在GC过程中保证数据的安全,我们在开始三色标记之前就会加上STW,在扫描确定黑白对象之后再放开STW。但是很明显这样的GC扫描的性能实在是太低了。

那么Go是如何解决标记-清除(mark and sweep)算法中的卡顿(stw,stop the world)问题的呢?

三、没有STW的三色标记

假如没有STW,会发生什么?有两种情况,在三色标记法中,是不希望被发生的:

  • 白色对象被黑色对象引用(白色挂到黑色下)。
  • 灰色与他的可达白色对象的关系遭到破坏(灰色丢失白色)。

如果这两种之一的情况发生,就会丢失对象。

四、屏障机制

让GC回收器满足以下两种情况之一,就可以保证对象不丢失:

  • 强三色不变式:强制不准黑色对象引用白色对象。
  • 弱三色不变式:被黑色引用的对象要处于灰色对象的保护下。

为了遵循上述两个不变式,GC引进了两种屏障方式:

  • 插入屏障
    • 被引用的对象会被标记为灰色;
    • 满足了强三色不变式。
    • 对象在内存槽有两种位置,栈和堆,栈空间容量小,但是要求响应速度快,所以插入屏障在栈空间对象操作中不使用。
    • 对于堆空间可以直接使用插入屏障,但是对于栈空间只能使用STW的方式。
  • 删除屏障
    • 被删除的对象,如果自身为白色,则标记为灰色。
    • 满足了弱三色不变式。
    • 回收精度低,对象被删除了,但是还是能在这一轮存活,下一轮才被删除。

五、混合写屏障(hybrid write barrier)

插入写屏障的短板:结束时需要STW来重新扫描栈。

删除写屏障的短板:回收精度低,GC在开始时STW扫描堆栈来记录初始快照,这个过程会保护开始时刻的所有存货对象。

混合写屏障结合了两者的优点,避免重新扫描的过程,减少了STW的时间。具体操作:

  1. 将栈上的所有可达对象标记为黑色(不重复扫描,无需STW);
  2. GC期间,任何栈上新创建的对象,均为黑色;
  3. 被删除的对象标记为灰色;
  4. 添加的对象标记为灰色。

参考:Golang三色标记混合写屏障GC模式全分析