关于GC
即垃圾回收机制,对于高级编程语言来说比较重要,尤其是业务性比较强的语言,比如python,java,可以让程序员更加关注业务而非内存管理本身,降低开发成本。
Go V1.3的标记清除法
流程
如图,首先是程序与对象之间的可达关系,可以看出目前的可达对象有1-2-3,4-7等五个对象,当触发GC机制的时候,会先进行STW操作进行暂停,之后对1-23,4-7等五个对象打上标记。而没有标记的对象没有被程序需要,就是垃圾会被回收。接下来STW结束,继续跑。
缺点
- 需要进行STW操作,让程序暂停,程序会出现卡顿(重要问题)
- 标记需要扫描整个heap
- 清楚数据会产生heap碎片
改进
早期的go语言尝试了减小STW的暂停范围来减少STW所需要的时间。
通常在STW之后,要经过启动STW->标记->清除->停止STW的流程,而清除事实上可以放到停止STW之后再进行,另开一个线程进行清除操作。
但是这样还是无法满足需要,所以采用了新的方法来代替标记清除法。
Go V1.5三色标记法
流程
第一步,首先只要是新创建的对象,默认的颜色就是被标记为白色。
第二步,每次的GC回收开始,然后从根节点开始遍历所有对象,把遍历到的对象从白色集合放入灰色结合
第三步,遍历所有的灰色集合,把灰色对象引用的对象从白色集合放入到灰色集合之中,之后把这个灰色对象放入到黑色集合之中。
第四步,重复第三步,直到灰色之中无任何对象,整个节点只有白色和黑色节点。
第五步,回收白色的垃圾节点。
无STW时出现的问题
- 一个白色对象被黑色对象引用(白色被挂在黑色下)
- 灰色对象与它之间的可达关系的白色对象遭到破坏(灰色同时丢失了该白色)
当同时满足这两个条件的时候,就会出现对象丢失的现象。
比如在这张图里面,当对象4(黑色)引用了对象3,而对象2恰好对对象3取消引用的时候,对象3就不会被遍历,导致被回收清除。
三色不变式
为了破坏三色标记法的两个条件,出现了三色不变式这一机制。
强三色不变式
破坏条件1,强制性的不允许黑色对象引用白色对象
弱三色不变式
破坏条件2
屏障机制
插入屏障
当对象被引用时,触发机制,具体操作为:在A对象引用B对象的时候,B对象被标记为灰色。(把B挂在A下游,B必须被标记为灰色)这样满足强三色不变式,因为不会存在黑色对象引用白色对象的情况了,因为白色会强制变成灰色)
但是插入屏障只会针对堆上的对象使用,栈上不会生效。因为栈的空间比较小,对性能要求比较高,如果每次 添加对象都要进行一次插入屏障判断,就会很影响性能。
所以为了保证栈上的空间可以正常完成GC,在准备回收白色之前,需要重新遍历扫描一次栈空间,此时加STW暂停保护栈,防止外界造成的干扰(有新的白色被黑色添加)
删除屏障
对象被删除时触发的机制,具体操作为:被删除的对象,如果自身为灰色或者白色,那么,被标记为灰色。
具体流程为:在开始GC 之前,必须 STW ,对整个根做一次起始快照。当赋值器(业务线程)从灰色或者白色对象中删除白色指针时候,写屏障会捕捉这一行为,将这一行为通知给回收器。
这么做可以满足弱三色不变式,保护灰色对象到白色对象的路径不被断开。
它的不足之处在于回收精度低,一个对象即使被删除了最后一个指向它的指针,仍然可以活过这一轮GC(因为这一轮它被标记为了灰色->黑色),在下一轮GC的时候被删除掉。
此外,删除屏障和插入屏障一样,不适合在栈空间上面进行,因为栈空间难以满足每一次都进行hook操作。
一个疑问
在这个例子中(黑色删除引用,指向一个之前不可达的白色对象),如果按照删除写屏障来不是会出错吗?
Go V1.8混合写屏障
在V1.8之后,使用的是由插入屏障 和删除屏障组成的混合写屏障
内容
- GC开始,把栈上的全部对象标记为黑色,之后便不再需要重新扫描。
- GC期间,任何在栈上新创建的对象都标记为黑色
- 写屏障把被删除的对象标记为灰色
- 写屏障把新添加的对象标记为灰色
其中,写屏障不在栈上面使用。
一些理解
混合写屏障主要解决了V1.5版本最后需要STW重新扫一边栈空间的问题。
采用的方法是通过1,2条,保证GC期间栈空间一直不出错,不需要最后重新STW一次,那么既然1,2可以避免了4的re-scan,为什么还要加入个3呢?
这里想到一个反例说明仍然需要3删除写屏障:
图中的上面三个输入栈空间,在GC开始的时候已经被打黑了,其它的都是堆空间的。
可以看到如果只有1,2,4的话,这里的a就会被误回收。
那么如果只有1,2,3会发生什么事呢?这里可以直接用上面删除写屏障的一个疑问来说明仍然不行。
所以,当同时满足1,2,3,4四条内容的时候,就可以在避免最后的STW re-scan的同时,保证不会发生误删除的现象。