Golang GC 三色标记和混合写屏障
大部分高级语言都有自己的垃圾回收机制,称之为 GC,Golang 的垃圾回收在多次迭代后才变成了现在这样的增加混合写屏障的三色标记法。
STW(stop the world):程序暂停,一般出现在GC中或者某些状态的保存过程,此时程序不停止的话可能会造成很多异常的情况,甚至于无法实现需求。
v1.3之前的标记清除法的实现和缺点
两个主要步骤:标记、清除。
- STW,然后找出不可达到的对象,打上标记
- 清除这些对象
- STW 结束,等待下一个 GC 周期
上述的逻辑简介但可行,对于 GC 来说也确实可以实现,但它有不少缺点。
- STW(stop the world):让程序暂停,程序出现卡顿 (重要问题)。
- 标记需要扫描整个 heap
- 清除数据会产生 heap 碎片
v1.3 版本之前的逻辑就在上方,为了降低 STW 的影响,v1.3 版本的标记清除逻辑为:
- STW,然后找出不可达到的对象,打上标记
- STW 结束,等待下一个 GC 周期
- 清除这些对象
但是这个方法还是治标不治本,STW的影响还是太大。
v1.5三色并发标记法
三色标记法 实际上就是通过三个阶段的标记来确定清除的对象都有哪些。
- 所有对象以及新创建的对象,都标记为白色
- GC 开始时,从根节点开始遍历,可以遍历到的对象标记为灰色
- 遍历灰色节点,灰色节点引用的对象也变成灰色节点,然后该灰色节点变成黑色节点
- 重复第三步,直到灰色对象全部变成黑色对象
- 回收白色对象。这时候的白色对象就没有被任何对象引用
没有 STW 的三色并发标记法
标记清除法一个很大的问题就是 STW,那么三色并发标记法可以抛弃 STW 吗?答案是否定的。
如果没有 STW,会出现对象丢失的问题,需要符合以下两个条件:
- 条件1: 一个白色对象被黑色对象引用(白色被挂在黑色下)
- 条件2: 灰色对象与它之间的可达关系的白色对象遭到破坏(灰色同时丢了该白色)
当符合条件1之后,这个黑色对象不会继续遍历下去,所以这个白色对象就有风险;当符合条件2之后,这个对象就必定会丢失。但实际上这个白色对象是被引用的。
需要一些新的机制来破坏上面两个条件。
屏障机制
强-弱 三色不等式
- 强三色不等式:不存在黑色对象引用到白色对象的指针,破坏条件1
- 弱三色不等式:所有被黑色对象引用的白色对象都处于灰色保护状态,破坏条件2,被引用对象至少被一个灰色对象引用
插入屏障
具体操作: 在A对象引用B对象的时候,B对象被标记为灰色。(将B挂在A下游,B必须被标记为灰色)
满足: 强三色不变式. (不存在黑色对象引用白色对象的情况了, 因为白色会强制变成灰色)
栈空间的特点是容量小,但是要求相应速度快,因为函数调用弹出频繁使用, 所以“插入屏障”机制,在栈空间的对象操作中不使用. 而仅仅使用在堆空间对象的操作中.
注意:在堆上启动插入屏障后,栈上的 GC 还是可能出现丢失对象,所以栈上的 GC 必须启动 STW。
删除屏障
具体操作: 被删除的对象,如果自身为灰色或者白色,那么被标记为灰色。
满足: 弱三色不变式. (保护灰色对象到白色对象的路径不会断)
它可以在栈上实现,但是精度低,一个对象即使被删除了最后一个指向它的指针也依旧可以活过这一轮,在下一轮GC中被清理掉。
v1.8 混合写屏障
插入写屏障和删除写屏障的短板:
- 插入写屏障:结束时需要 STW 来重新扫描栈,标记栈上引用的白色对象的存活;
- 删除写屏障:回收精度低,GC 开始时 STW 扫描堆栈来记录初始快照,这个过程会保护开始时刻的所有存活对象。
Go V1.8版本引入了混合写屏障机制(hybrid write barrier),避免了对栈re-scan的过程,极大的减少了STW的时间。结合了两者的优点。
具体操作:
- GC 开始将栈上的对象全部扫描并标记为黑色(之后不再进行第二次重复扫描,无需 STW),
- GC 期间,任何在栈上创建的新对象,均为黑色。
- 被删除的对象标记为灰色。
- 被添加的对象标记为灰色。
满足: 变形的弱三色不变式.
通过前两部操作可以看出,只要是在栈上的对象,都是安全的,直接在协程退出后进行清理,屏障机制只针对堆上的对象进行。在整个过程中不需要进行 STW,所以整体效率很高。