Go V1.3之前的GC采用标记清扫方法(mark and sweep),在GC的时候暂停所有的线程,标记出活动的对象,清除不活动的对象,待回收完成后再恢复到之前的工作环境,这就是STW(Stop The World)。
STW的大致流程是这样的:
graph LR
1.启动SWT --> 2.Mark标记 --> 3.Sweep清除 --> 4.停止STW --> 5.程序继续执行
标记清扫方法的缺点:
最重要的问题还是STW会影响业务,所以要尽量取消STW,或减少STW的时间。可以通过调整第四步和第三步骤执行顺序,缩小STW的范围,清理的时候和程序并行去执行。
graph LR
1.启动SWT --> 2.Mark标记 --> 3.停止STW --> 4.Sweep清除,程序继续并行执行
在标记过程中还是比较浪费时间,能不能尝试采用新的标记模式来替代清除标记法呢?
Go V1.5版本开始使用了三色标记法。三色标记法会在整个的GC里面记录三个集合:
一开始,先将所有对象标记为白色。从程序对象根集合中开始遍历一层,找到的对象标记为灰色。然后将得到的灰色节点遍历一层,找到的对象标记为灰色,原灰色节点变为黑色。不断重复,直到不存在灰色对象,即直到只剩下白色和黑色对象。最终剩下的白色对象就是不可达的对象,也就是要清理的对象。
例如,程序中有以下对象:
graph LR
RootSet --> 对象1 --> 对象2 --> 对象3
RootSet --> 对象4 --> 对象7
对象5 --> 对象2
对象6:::white
classDef white fill:#fff
到此时白色标记集合中只有 对象5 和 对象6,它们就是不可达的对象,最终回收掉白色的 对象5 和 对象6。
上面三色标记的过程理论上还是在STW的保护下。如果不使用STW的话,程序是在并行运行的,可能会在程序删除的同时恰巧这个对象被引用到了,这就会产生致命的问题。那它还是性能比较低的一种GC算法。那三色标记可以不可以不启动SWT?
那先看一下三色标记最不希望发生的两件事情:
当这两个条件同时满足时,就会出现对象丢失的现象。这也是不使用STW可能会发生的事情。
为了不发生这种情况,最简单的方式还是使用STW。可是STW的过程有明显的资源浪费,对用户程序也有很大影响,那就要想办法保证对象不丢失的情况下尽可能提高GC效率,减少STW时间。
应对上面两种情况,Go团队研究了强、弱三色不变式两种方式来丢失对象。
强三色不变式:强制性的不允许黑色对象引用白色对象。(破坏了上面的条件1)
flowchart LR
black[黑色] --x white[白色]
style black fill:#000,color:#fff
style white fill:#fff
弱三色不变式:黑色可以引用白色,但是要存在其它灰色对象对该白色对象的引用,或者它的链路上游存在灰色对象,这样可以保证这个白色对象不会丢失。
graph LR
黑色:::black --> 白色:::white
灰色:::gray --> 白色:::white
classDef black fill:#000,color:#fff
classDef white fill:#fff
classDef gray fill:#ccc
graph LR
黑色:::black --> 白色1:::white
灰色:::gray --> 白色2:::white --> 白色1:::white
classDef black fill:#000,color:#fff
classDef white fill:#fff
classDef gray fill:#ccc
在三色标记中,如果满足强/弱之一,即可保证对象不丢失。
什么是屏障?在程序垃圾回收的过程中,不影响正常业务逻辑的情况下,加入一些额外的判断,来遏制两种情况的发生,它和hook、回调、handler的思想是类似的。在三色标记法中,有两种屏障机制:
在A对象引用B对象的时候,会触发插入屏障判断,B对象被标记为灰色。这样就不存在黑色对象引用白色对象了,因为白色会强制变成灰色。它满足了强三色不变式。
那每创建一个对象,难道都要去做这样的一个判断吗,这样会影响整个程序的性能的。由于栈的空间比较小,为了不影响栈的运行效率,在栈上操作是不会触发插入屏障的,而堆上是启用插入屏障的。
对于栈在白色对象被回收之前需要短暂的启用STW机制,结束时候需要SWT来重新扫描栈,这也是插入写屏障的不足之处,大约需要10-100ms.
被删除的对象,如果自身为灰色或者白色,那么被标记为灰色。它是为了满足弱三色不变式(保护灰色对象到白色对象的路径不会断)。
例如:
graph LR
RootSet --> 对象1 --> 对象2 --> 对象3 --> 对象4
classDef white fill:#fff
本轮GC检测没有回收对象,等到第二轮GC的时候,会将对象2清理掉。这也是删除写屏障的不足,一个对象即使被删除了最后一个指向它的指针,依旧可以再活一轮,在下一轮GC中被清理掉。
上面了解到了插入写屏障和删除写屏障的一些不足,插入写屏障需要STW重新扫描栈,删除写屏障回收精度低。
在 Go v1.8版本之后采用三色标记法,并引入了混合写屏障机制。混合写屏障结合了插入写屏障和删除写屏障的优点。了解下它的基本流程:
这4个操作的组合就达成了混合写屏障的机制,不需要STW也能保证一次GC的完成。它是一个变形的弱三色不变式,并结合了插入、删除写屏障两者的优点。
场景:堆对象7 被 堆对象4 删除引用,同时成为 栈对象1 的下游。
flowchart LR
栈[栈:不启用屏障] --> 对象1 --> 对象2 --> 对象3
堆[堆:启用屏障] --> 对象4 -.X删除引用X.-x 对象7
对象1 --添加引用--> 对象7
这样所有有效对象最终都会被标记为黑色,栈上也就不需要再次扫描了,也不用通过STW机制来进行保护了。
场景:对象3 被 栈对象2 删除引用,并成为另一个 栈对象9 的下游。
flowchart LR
栈[栈:不启用屏障] --> 对象1 --> 对象2 -.X删除引用X.-x 对象3
栈[栈:不启用屏障] --创建对象--> 对象9
对象9 --添加引用--> 对象3
如果 对象9 没有引用 对象3,对象2 直接删除了 对象3:这种情况下 对象3 会先变为灰色被保护。等到下一轮GC,对象3 就会被标记为白色对象,然后被清理。
场景:堆对象7 被 堆对象4 删除引用,成为另一个 堆对象10 的下游。
flowchart LR
堆[堆:启用屏障] --> 对象4 -.X删除引用X.-x 对象7 --> 对象8
堆[堆:启用屏障] ---> 对象10 --> 对象11
对象10 --添加引用--> 对象7
场景:对象2 从一个 栈对象1 删除引用,成为另一个 堆对象4 的下游。
//伪代码
栈对象1.对象2 = nuLl
堆对象4.对象2 = 栈对象2
堆对象4.对象7 = null
flowchart LR
栈[栈:不启用屏障] --> 对象1 -.X删除引用X.-x 对象2 --> 对象3
堆[堆:启用屏障] --> 对象4 -.X删除引用X.-x 对象7 --> 对象8
对象4 --添加引用--> 对象2