阅读了一堆文章,但是对于GC(garbage collector 垃圾回收),尤其是混合读写屏障讲解的都比较模糊,下面进行总结。

基本的标记清除算法

图1:三色标记

垃圾回收算法要回收不可达的对象,可以使用三色标记的算法。

  • Root对象引用的对象(A,B)标记为灰色。
  • 灰色对象标记为黑色,其引用的对象(D)标记为灰色。
  • 迭代进行,直到没有灰色对象,此时白色对象(C,E,F)为不可达对象,内存需要被释放。

上述GC过程中引用不可以发生变化,

如果发生变化,黑色对象引用白色对象,则白色对象释放以后,出现内存泄漏,所以需要停止所有的goroutine才可以进行GC(stop the world STW)。为了不使用stop the world,可以引入屏障机制。

三色不变式

即使引用发生变化,只要满足三色不变式,就可以保证不出现内存泄漏的情况。

强三色不变式:黑色对象只能引用灰色对象。

弱三色不变式:黑色对象可以引用白色对象,但是白色对象必须受到灰色对象的保护,及其父节点存在灰色对象,这样的白色节点一定会最终被标记为黑色,不会出现内存泄漏。

只要满足强弱三色不变式的任意一个即可不出现内存泄漏的情况

屏障机制

注意:对象分配内存可能在堆上也可能在栈上,屏障机制只作用于堆,由于效率原因栈不开启屏障

插入写屏障

流程:在A对象引用B对象的时候,B对象被标记为灰色。

  1. 纯粹的插入写屏障是满足强三色不变式的(永远不会出现黑色对象指向白色对象);
  2. 但是由于栈上对象无写屏障,那么导致黑色的栈可能指向白色的堆对象,必须 STW 重新扫描栈(黑色栈对象引用的白色对象在新的一轮扫描中被标记为黑色,不回收);
  3. STW 重新扫描栈再 goroutine 量大且活跃的场景,延迟不可控,经验值平均 10-100ms;

删除写屏障

流程:被删除引用的对象,如果为白色,那么被标记为灰色。

  1. 如果一个黑色对象添加一个向白色对象的引用,说明这个白色对象可达,受到别的对象的保护,不需要处理,只有在删除引用的时候,才需要处理,保证原先被保护的对象依然受到保护,保证弱三色不变式
  2. 必须在起始时,STW 扫描所有goroutine的栈,全部扫黑,保证所有堆上在用的对象都处于灰色保护下。
  3. 由于起始需要执行 STW,删除写屏障不适用于栈特别大的场景,栈越大,STW 扫描时间越长。
  4. 回收精度低。

是否可以不STW,每个栈单独进行扫黑(每个goroutine有单独的栈)?不可以,有下面的例子:对于两个goroutine,四个对象:

如果逐个goroutine扫描,则会有以下情况:

经过上面的步骤得到下图:

图2 如果不进行栈扫黑

有黑色指向白色,内存泄漏。

解决方案1:STW,停止所有goroutine,对栈进行扫黑:

无法构成上述错误情况,所有堆的对象被灰色节点保护,使用删除写屏障即可。

解决方案2:使用插入写屏障,从而可以使得栈扫黑的过程可以不停止所有goroutine。(混合写屏障)

混合写屏障

步骤:GC开始时栈扫黑,每个栈单独扫描,无需STW。堆上执行插入写和删除写屏障。

本质上,混合写屏障已经可以不使用STW了。

参考