“强-弱” 三色不变式
上篇讲到如果在三色标记法去掉STW环节之后,可能会发生对象丢失现象,即一个合法引用的对象被gc给当作垃圾对象错误回收掉了。
而为了避免这种情况的出现需要破坏这种现象形成的两个前提条件:
- 条件1: 一个白色对象被黑色对象引用(白色被挂在黑色下)
- 条件2: 灰色对象与它之间的可达关系的白色对象遭到破坏(灰色同时丢了该白色)
强三色不变式
如果我们破坏第一个条件,即强制性的不允许黑色对象引用白色对象,这样就不会出现有白色对象被误删的情况,这个就叫做强三色不变式。
弱三色不变式
假设允许第一个条件,只破坏第二个条件,即黑色对象可以引用白色对象,但是这个白色对象必须存在其他灰色对象对它的引用,或者可达它的链路上游存在灰色对象。这个就叫做弱三色不变式。
这样实则是黑色对象引用白色对象,白色对象处于一个危险被删除的状态,但是上游灰色对象的引用,可以保护该白色对象,使其安全。
为了遵循上述的两个方式,GC算法演进到两种屏障方式,他们分别是“插入屏障”和“删除屏障”。
插入屏障(满足强三色不变式)
- 具体操作:
在A对象引用B对象的时候,B对象被标记为灰色。(将B挂在A下游,B必须被标记为灰色)
- 满足: 强三色不变式
不存在黑色对象引用白色对象的情况了, 因为白色会强制变成灰色
- 实现伪代码:
A.添加下游对象(nil, B) //A 之前没有下游, 新添加一个下游对象B, B被标记为灰色
A.添加下游对象(C, B) //A 将下游对象C 更换为B, B被标记为灰色
这段伪码逻辑就是写屏障,. 我们知道,黑色对象的内存槽有两种位置, 栈和堆. 栈空间的特点是容量小,但是要求相应速度快,因为函数调用弹出频繁使用, 所以“插入屏障”机制,在栈空间的对象操作中不使用. 而仅仅使用在堆空间对象的操作中.
但是如果栈不添加,当全部三色标记扫描之后,栈上有可能依然存在白色对象被引用的情况,所以要对栈重新进行三色标记扫描, 但这次为了对象不丢失, 要对本次标记扫描启动STW暂停. 直到栈空间的三色标记结束.
删除屏障(满足弱三色不变式)
- 具体操作
被删除的对象,如果自身为灰色或者白色,那么被标记为灰色。
- 满足: 弱三色不变式
保护灰色对象到白色对象的路径不会断
- 实现伪代码:
A.添加下游对象(B, nil) //A对象,删除B对象的引用。 B被A删除,被标记为灰(如果B之前为白)
A.添加下游对象(B, C) //A对象,更换下游B变成C。 B被A删除,被标记为灰(如果B之前为白)
这种方式的回收精度低,一个对象即使被删除了最后一个指向它的指针也依旧可以活过这一轮,在下一轮GC中被清理掉。
混合写屏障(Go1.8引入)
插入写屏障和删除写屏障的短板:
- 插入写屏障:结束时需要STW来重新扫描栈,标记栈上引用的白色对象的存活;
- 删除写屏障:回收精度低,GC开始时STW扫描堆栈来记录初始快照,这个过程会保护开始时刻的所有存活对象。
Go V1.8版本引入了混合写屏障机制(hybrid write barrier),避免了对栈re-scan的过程,极大的减少了STW的时间。结合了两者的优点。
混合写屏障规则
具体操作:
1、GC开始将栈上的对象全部扫描并标记为黑色(之后不再进行第二次重复扫描,无需STW),
2、GC期间,任何在栈上创建的新对象,均为黑色。
3、被删除的对象标记为灰色。
4、被添加的对象标记为灰色。
满足: 变形的弱三色不变式
伪代码
添加下游对象(当前下游对象slot, 新下游对象ptr) {
//1
标记灰色(当前下游对象slot) //只要当前下游对象被移走,就标记灰色
//2
标记灰色(新下游对象ptr)
//3
当前下游对象slot = 新下游对象ptr
}
小结
- GoV1.3- 普通标记清除法,整体过程需要启动STW,效率极低。
- GoV1.5- 三色标记法, 堆空间启动写屏障,栈空间不启动,全部扫描之后,需要重新扫描一次栈(需要STW),效率普通
- GoV1.8-三色标记法,混合写屏障机制, 栈空间不启动,堆空间启动。整个过程几乎不需要STW,效率较高。