垃圾回收(Garbage Collection,简称GC)是编程语言中提供的自动的内存管理机制,自动释放不需要的内存对象,让出存储器资源。GC过程中无需程序员手动执行,GC机制在现代很多编程语言都支持,GC能力的性能与优劣也是不同语言之间对比度指标之一。
Golang在GC的演进过程中也经历了很多次变革,截止在GoV1.8之前,Golang的GC改动还是非常的大,具体的几次重大改变如下:
(1)Go V1.3之前的标记-清除(mark and sweep)法。
(2)Go V1.5的三色并发标记法。
(3)Go V1.5的 “强-弱” 三色不变式、插入屏障、删除屏障。
(4)Go V1.8的混合写屏障机制。
本章节将介绍每种GC的算法模型和各自优缺点,以及Golang的GC是如何从逻辑需求上一步步演进到混合写屏障的模式。
一
Go V1.3标记-清除算法
接下来我们来看一下在Golang V1.3之前的时候主要用的普通的标记-清除算法,此算法主要有两个步骤:
①标记(Mark phase)。
②清除(Sweep phase)。
过程也非常清晰明了,实则是找到需要被清除的内存数据,然后一次性清除,下面我们来说明一下具体的过程。
1. 标记清除(mark and sweep)算法的详细过程
第一步,暂停程序业务逻辑, 分类出可达和不可达的对象,然后做上标记,如图1.1所示。
图1.1 标记清除回收1
图中表示是程序与对象的可达关系,目前程序的可达对象有对象1-2-3,对象4-7等五个对象。
第二步, 开始标记,程序找出它所有可达的对象,并做上标记。如图1.2所示。
图1.2 标记清除回收2
所以对象1-2-3、对象4-7等五个对象被做上标记。
第三步, 标记完了之后,然后开始清除未标记的对象,如图1.3所示。
图1.3 标记清除回收3
操作非常简单,但是有一点需要额外注意,mark and sweep算法在执行的时候,需要程序暂停!即 STW(stop the world),STW的过程中,CPU不执行用户代码,全部用于垃圾回收,这个过程的影响很大,所以STW也是一些回收机制最大的难题和希望优化的点。所以在执行第三步的这段时间,程序会暂定停止任何工作,卡在那等待回收执行完毕。
第四步, 停止暂停,让程序继续跑。然后循环重复这个过程,直到process程序生命周期结束。
以上便是标记-清除(mark and sweep)回收的算法。
2. 标记清除(mark and sweep)算法的缺点
标记清除算法明了,过程鲜明干脆,但是也有非常严重的问题,第一是STW(stop the world)这让程序暂停,程序也会出现卡顿 (重要问题);第二是标记需要扫描整个heap;第三是清除数据会产生heap碎片。
Go V1.3版本之前就是以上来实施的, 在执行GC的基本流程就是首先启动STW暂停,然后执行标记,再执行数据回收,最后停止STW,如图1.4所示。
图1.4 STW暂停范围
从上图来看,全部的GC时间都是包裹在STW范围之内的,这样貌似程序暂停的时间过长,影响程序的运行性能。所以Go V1.3 做了简单的优化,将STW的步骤提前, 减少STW暂停的时间范围。如图1.5所示。
图1.5 STW优化后的暂停范围
上图主要是将STW的步骤提前了异步,因为在Sweep清除的时候,可以不需要STW停止,因为这些对象已经是不可达对象了,不会出现回收写冲突等问题。
但是无论怎么优化,Go V1.3都面临这么一个重要问题,就是mark-and-sweep算法会暂停整个程序 。
Go是如何面对并这个问题的呢?接下来G V1.5版本 就用三色并发标记法来优化这个问题。
二
Go V1.5的三色标记法
Golang中的垃圾回收主要应用三色标记法,GC过程和其他用户Goroutine可并发运行,但需要一定时间的STW(stop the world),所谓三色标记法实际上就是通过三个阶段的标记来确定清楚的对象都有哪些?我们来看一下具体的过程。
1. 三色标记法的过程
第一步 , 每次新创建的对象,默认的颜色都是标记为“白色”,如图1.6所示。
图1.6 三色标记法1
上图所示,我们的程序可抵达的内存对象关系如左图所示,右边的标记表,是用来记录目前每个对象的标记颜色分类。这里面需要注意的是,所谓“程序”,则是一些对象的跟节点集合。所以我们如果将“程序”展开,会得到类似如下的表现形式,如图1.7所示。
图1.7 三色标记法2
第二步, 每次GC回收开始,会从根节点开始遍历所有对象,把遍历到的对象从白色集合放入“灰色”集合如图1.8所示。
图1.8 三色标记法3
这里要注意的是,本次遍历是一次遍历,非递归形式,是从程序抽次可抵达的对象遍历一层,如上图所示,当前可抵达的对象是对象1和对象4,那么自然本轮遍历结束,对象1和对象4就会被标记为灰色,灰色标记表就会多出这两个对象。
第三步, 遍历灰色集合,将灰色对象引用的对象从白色集合放入灰色集合,之后将此灰色对象放入黑色集合,如图1.9所示。
图1.9 三色标记法4
这一次遍历是只扫描灰色对象,将灰色对象的第一层遍历可抵达的对象由白色变为灰色,如:对象2、对象7。而之前的灰色对象1和对象4则会被标记为黑色,同时由灰色标记表移动到黑色标记表中。
第四步, 重复第三步, 直到灰色中无任何对象,如图1.10和1.11所示。
图1.10 三色标记法5
图1.11 三色标记法6
当我们全部的可达对象都遍历完后,灰色标记表将不再存在灰色对象,目前全部内存的数据只有两种颜色,黑色和白色。那么黑色对象就是我们程序逻辑可达(需要的)对象,这些数据是目前支撑程序正常业务运行的,是合法的有用数据,不可删除,白色的对象是全部不可达对象,目前程序逻辑并不依赖他们,那么白色对象就是内存中目前的垃圾数据,需要被清除。
第五步: 回收所有的白色标记表的对象. 也就是回收垃圾,如图1.12所示。
图1.12 三色标记法7
我们将全部的白色对象进行删除回收,剩下的就是全部依赖的黑色对象。
以上便是 三色并发标记法 ,不难看出,我们上面已经清楚的体现 三色 的特性。但是这里面可能会有很多并发流程均会被扫描,执行并发流程的内存可能相互依赖,为了在GC过程中保证数据的安全,我们在开始三色标记之前就会加上STW,在扫描确定黑白对象之后再放开STW。但是很明显这样的GC扫描的性能实在是太低了。
那么Go是如何解决标记-清除(mark and sweep)算法中的卡顿(stw,stop the world)问题的呢?
2. 没有STW的三色标记法
先抛砖引玉,我们假如如果没有STW,那么也就不会再存在性能上的问题,那么接下来我们假设如果三色标记法不加入STW会发生什么事情?
我们还是基于上述的三色并发标记法来说,他是一定要依赖STW的。因为如果不暂停程序, 程序的逻辑改变对象引用关系, 这种动作如果在标记阶段做了修改,会影响标记结果的正确性,我们来看看一个场景,如果三色标记法, 标记过程不使用STW将会发生什么事情?
我们把初始状态设置为已经经历了第一轮扫描,目前黑色的有对象1和对象4, 灰色的有对象2和对象7,其他的为白色对象,且对象2是通过指针p指向对象3的,如图1.13所示。
图1.13 没有STW的三色标记法1
现在如何三色标记过程不启动STW,那么在GC扫描过程中,任意的对象均可能发生读写操作,如图所示,在还没有扫描到对象2的时候,已经标记为黑色的对象4,此时创建指针q,并且指向白色的对象3。
图1.14 没有STW的三色标记法2
与此同时灰色的对象2将指针p移除,那么白色的对象3实则就是被挂在了已经扫描完成的黑色的对象4下,如图1.15所示。
图1.15 没有STW的三色标记法3
然后我们正常指向三色标记的算法逻辑,将所有灰色的对象标记为黑色,那么对象2和对象7就被标记成了黑色,如图1.16所示。
图1.16 没有STW的三色标记法4
那么就执行了三色标记的最后一步,将所有白色对象当做垃圾进行回收,如图1.17所示。
图1.17 没有STW的三色标记法5
但是最后我们才发现,本来是对象4合法引用的对象3,却被GC给“误杀”回收掉了。
3. 触发三色标记法不安全的必要条件
可以看出,有两种情况,在三色标记法中,是不希望被发生的。
(1)条件1:一个白色对象被黑色对象引用(白色被挂在黑色下)。
(2)条件2:灰色对象与它之间的可达关系的白色对象遭到破坏(灰色同时丢了该白色)。
如果当以上两个条件同时满足时,就会出现对象丢失现象!
并且,如上图所示的场景中,如果示例中的白色对象3还有很多下游对象的话, 也会一并都清理掉。
为了防止这种现象的发生,最简单的方式就是STW,直接禁止掉其他用户程序对对象引用关系的干扰,但是STW的过程有明显的资源浪费,对所有的用户程序都有很大影响。那么是否可以在保证对象不丢失的情况下合理的尽可能的提高GC效率,减少STW时间呢?答案是可以的,我们只要使用一种机制,尝试去破坏上面的两个必要条件就可以了。
三
Go V1.5 的屏障机制
我们让GC回收器,满足下面两种情况之一时,即可保对象不丢失。这两种方式就是“强三色不变式”和“弱三色不变式”。
1. “强-弱”三色不变式
(1)强三色不变式
不存在黑色对象引用到白色对象的指针,如图1.18所示。
图1.18 强三色不变式
强三色不变色实际上是强制性的不允许黑色对象引用白色对象,这样就不会出现有白色对象被误删的情况。
(2)弱三色不变式
所有被黑色对象引用的白色对象都处于灰色保护状态,如图1.19所示。
图1.19 弱三色不变式
弱三色不变式强调,黑色对象可以引用白色对象,但是这个白色对象必须存在其他灰色对象对它的引用,或者可达它的链路上游存在灰色对象。这样实则是黑色对象引用白色对象,白色对象处于一个危险被删除的状态,但是上游灰色对象的引用,可以保护该白色对象,使其安全。
为了遵循上述的两个方式,GC算法演进到两种屏障方式,他们“插入屏障”和“删除屏障”。
2. 插入屏障
插入屏障的具体操作是,在A对象引用B对象的时候,B对象被标记为灰色。(将B挂在A下游,B必须被标记为灰色)。
插入屏障实际上是满足强三色不变式(不存在黑色对象引用白色对象的情况了, 因为白色会强制变成灰色)。插入屏障的伪码如下:
添加下游对象(当前下游对象slot, 新下游对象ptr) {
//1
标记灰色(新下游对象ptr)
//2
当前下游对象slot = 新下游对象ptr
}
插入写屏障的伪代码的场景如下:
//A 之前没有下游,新添加一个下游对象B,B被标记为灰色
A.添加下游对象(nil, B)
//A 将下游对象C 更换为B,B被标记为灰色
A.添加下游对象(C, B)
这段伪码逻辑就是写屏障。我们知道,黑色对象的内存槽有两种位置:栈和堆。栈空间的特点是容量小,但是要求相应速度快,因为函数调用弹出频繁使用, 所以“插入屏障”机制在栈空间的对象操作中不使用,而仅仅使用在堆空间对象的操作中。接下来,我们用几张图,来模拟一个详细的过程, 希望您能够更客观的看清晰整体流程。
我们目前还是假设程序初创建,其中栈空间的对象有对象1,对象2,对象3和对象5,其中对象1引用对象2,对象2引用对象3,对象3没有下游对象,而对象5引用对象2。堆空间有对象4引用对象7,对象7没有下游对象,对象6没有引用任何对象,也没有被任何对象引用。这些内存对象全部标记为白色,那么白色标记表中将全部的对象装入其中,如图1.20所示。
图1.20 插入屏障流程1
我们依然依据三色标记的流程,遍历Root Set根节点集合,非递归形式,只遍历一次,我们能够标记出第一层的灰色节点对象1和对象4,同时这些灰色节点也被添加至灰色标记表中,如图1.21所示。
图1.21 插入屏障流程2
按照三色标记法的顺序来,接下来就遍历灰色标记表中的对象1和对象4,将可达的对象,从白色标记为灰色。同时被遍历的灰色对象,就被标记为黑色,如图1.22所示。
图1.22 插入屏障流程3
由于并发的特性,此刻外界向已经标记为黑色的对象4添加白色的对象8,向已经标记为黑色的对象1添加下游白色的对象9,如图1.23所示。我们知道,对象1是栈空间,根据我们插入屏障的特点,为了性能的保证,栈空间创建对象不触发插入屏障。但是对象4是在堆空间,那么对象4即将触发插入屏障机制。
图1.23 插入屏障流程4
由于插入写屏障的机制(黑色对象添加白色对象,那么将白色对象改为灰色),所以对于堆上的对象4添加对象8的时候,那么对象8将被标记为灰色,而对象9依然是白色,如图1.24所示。
图1.24 插入屏障流程5
之后就是正常的三色标记流程,继续循环上述的流程,直到没有灰色节点,那么目前我们得到的目前的对象状态如图4.24所示,栈空间的对象1,对象2,对象3被标记为黑色,堆空间上的对象4,对象7,对象8被标记为黑色。而其他的对象9、对象5和对象6依然是白色对象。
图1.25 插入屏障流程6
这个时候插入屏障并不会立刻执行垃圾回收动作,而是会做一个额外的过程。但是如果栈不添加,当全部三色标记扫描之后,栈上有可能依然存在白色对象被引用的情况(如上图的对象9)。所以要对栈重新进行三色标记扫描,但这次为了对象不丢失。但这次扫描要启动STW暂停,直到栈空间的三色标记结束。
如图1.26所示,栈空间的全部内存对象均被重新标记为白色(对象1、对象2、对象3、对象9、对象5),而且会对这些白色对象启动STW暂定保护起来,那么任何并行对以上被保护的对象进行任何读写操作均会被拦截且阻塞,防止外界干扰(如:有新的白色被对象被黑色对象添加)。
与此同时,对于其他堆空间的对象将不会触发STW,这样也是为了保证堆空间的GC回收性能。
图1.26 插入屏障流程7
接下来就是将STW所保护的区域内,继续执行三色标记流程,直到全部可达白色对象都被扫描到,直到没有灰色节点。最后我们得到一个最终的状态,对象1、对象2、对象3和对象9均被标记为黑色,由于对象5依然没有被扫描到,那么对象5依然是白色如图1.27所示。
图1.27 插入屏障流程8
这回当全部内存对象的颜色只有白色和黑色的时候,那么我们就会停止STW,释放保护层,如图1.28所示。
图1.28 插入屏障流程9
最后将栈和堆空间扫描剩余的全部白色节点(对象5和对象6)回收清除。一般我们在栈空间的这次STW的时间大约在10~100ms(毫秒)之间。
那么在最后的扫描之后内存中将全部为黑色对象,如图1.29所示。那么整体的基于插入屏障的三色标记回收机制的流程就介绍完了。
图1.29 插入屏障流程10
插入屏障的目的是为了保证黑色对象插入的时候有灰色对象对其保护,或者将被插入的对象变为灰色,插入屏障实则是满足强三色不变式的一种表现,这样就不会出现被误删的白色对象。
3. 删除屏障
删除屏障的具体操作是,被删除的对象,如果自身为灰色或者白色,那么被标记为灰色。
删除屏障实际上是满足弱三色不变式。删除(保护灰色对象到白色对象的路径不会断),删除屏障的伪代码如下:
添加下游对象(当前下游对象slot, 新下游对象ptr) {
//1
if (当前下游对象slot是灰色 || 当前下游对象slot是白色) {
//slot为被删除对象, 标记为灰色
标记灰色(当前下游对象slot)
}
//2
当前下游对象slot = 新下游对象ptr
}
删除屏障的伪代码场景如下:
//A对象,删除B对象的引用。B被A删除,被标记为灰(如果B之前为白)
A. 添加下游对象(B, nil)
//A对象,更换下游B变成C。B被A删除,被标记为灰(如果B之前为白)
A. 添加下游对象(B, C)
删除屏障是当一个对象的引用被摘掉的时候,或者当一个对象引用被上游替换的时候,该对象被标记为灰色。标记为灰色的目的是为了让被删除的白色对象期间,如果又被其他的黑色对象引用,被删除对象本没有变成应被会受到资源而被回收掉。接下来,我们用几张图,来模拟一个详细的过程, 希望您能够更可观的看清晰整体流程。
如图1.30所示,在开始执行删除屏障的三色标记之前,目前的内存情况如下,在栈空间有对象1引用对象5、对象5引用对象2、对象2引用对象3,对象3没有下游对象。在堆空间有对象4引用对象7,对象7没有下游对象。没有人引用对象6,对象6也没有下游对象。以上对象全部都被标记为白色,且加入到白色标记表中。
图1.30 删除屏障流程1
我们依然依据三色标记的流程,遍历Root Set根节点集合,非递归形式,只遍历一次,我们能够标记出第一层的灰色节点对象1和对象4,同时这些灰色节点也被添加至灰色标记表中,如图1.31所示。
图1.31 删除屏障流程2
如果此时灰色对象1删除白色对象5,如果我们不触发删除屏障机制,那么白色对象5连同下游对象2和对象3将于主链路断开,最终也会被清除,如图1.32所示。
图1.32 删除屏障流程3
但是目前的三色标记法是删除屏障机制,那么我们依照算法,被删除的对象将被标记为灰色,目的是为了保护对象5和下游对象(思考为什么需要保护,如果不标记对象5为灰色会出现哪些意外问题?其中有一种情况是,如图1.32所示,假如对象1已经删除了对象5,对象5依旧是白色,那么由于整体流程没有加STW保护,那么极有可能在删除的过程中,同一时刻有一个已经被标记为黑色的对象引用了这个对象5,那么对象5依然是程序流程中需要依赖的合法内存对象,但是却会被最终按照白色对象被GC回收掉,因为黑色的下游对象并不会被保护起来),将对象5标记成了灰色,如图1.33所示。
图1.33 删除屏障流程4
按照三色标记法的顺序,接下来就遍历灰色标记表中的对象1、对象4和对象5,将它们可达的对象,从白色标记为灰色。同时被遍历的灰色对象,就被标记为黑色,如图1.34所示,那么这轮流程下来后,对象1、对象4和对象5被标记为黑色,对象2和对象7被标记为灰色,对象3和对象6依旧是白色。
图1.34 删除屏障流程5
继续循环上述流程进行三色标记,直到没有灰色节点,最终的状态如图1.35所示,除了对象6,全部的节点均被标记成黑色。
图1.35 删除屏障流程6
最后,我们执行回收清除流程,将白色对象全部通过GC回收处理,如图1.36所示。
图1.36 删除屏障流程7
以上便是三色标记利用删除屏障的处理流程,删除屏障依旧可以满足并行状态下的垃圾回收动作,但是这种方式的回收精度较低,因为一个对象即使被删除了,但是最后一个指向它的指针也依旧可以多“活”过这一轮,只有等到下一轮GC中才会被清理掉。
四
Go V1.8的混合写屏障
插入写屏障和删除写屏障虽然都可以在一定程度上解决STW带来的无法并行处理的问题,但是他们也都有各自的短板。
(1)插入写屏障:结束时需要STW来重新扫描栈,标记栈上引用的白色对象的存活;
(2)删除写屏障:回收精度低,GC开始时STW扫描堆栈来记录初始快照,这个过程会保护开始时刻的所有存活对象。
Go V1.8版本引入了混合写屏障机制(hybrid write barrier),避免了对栈re-scan(重新扫描)的过程,这也极大的减少了STW的时间,同时结合了插入写屏障和删除写屏障两者的优点,本节将介绍混合写屏障的规则和一些混合写屏障触发的一些场景流程分析。
1. 混合写屏障(hybrid write barrier)规则
混合写屏障的具体操作一般需要遵循以下几个条件限制:
(1)GC开始将栈上的对象全部扫描并标记为黑色(之后不再进行第二次重复扫描,无须STW)。
(2)GC期间,任何在栈上创建的新对象,均为黑色。
(3)被删除的对象标记为灰色。
(4) 被添加的对象标记为灰色。
混合写屏障实际上满足的是一种变形的弱三色不变式。他的伪代码如下:
添加下游对象(当前下游对象slot, 新下游对象ptr) {
//1
标记灰色(当前下游对象slot) //只要当前下游对象被移走,就标记灰色
//2
标记灰色(新下游对象ptr)
//3
当前下游对象slot = 新下游对象ptr
}
这里我们注意,屏障技术是不在栈上应用的,因为要保证栈的运行效率。接下来,我们用几张图,来模拟混合写屏障的详细的过程, 希望您能够更客观的看清晰整体流程。注意混合写屏障是GC的一种屏障机制,所以只是当程序执行GC的时候,才会触发这种机制。
现在我们还是制造出一个场景来分析,当我们GC开始的时候,初始化的内存对象结构如下,栈空间范围有Root Set根节点集合引用对象1、对象2引用对象2、对象3下游没有被引用对象。对象5没有上游对象,且对象5引用对象8,对象8下游没有被引用对象。堆空间范围有Root Set根节点集合引用对象4,对象4引用对象7,对象7下游没有被引用对象。对象6没有上游对象,同时对象6下游也没有被引用对象。这些内存对象均被标记为白色,并且全部被放置在白色标记表中,如图1.37所示。
图1.37 混合写屏障流程1
现在GC开始,按照上述混合写屏障的几个步骤,那么他的第一步就是扫描栈区,将可达对象全部标记为黑色。所以扫描栈区结束的时候,对象1、对象2、对象3均可达,他们被标记成了黑色,同时也被加入到黑色标记表中,如图1.38所示。
图1.38 混合写屏障流程2
那么接下来我们就来分析一些混合写屏障的一些场景,本节会列出四种场景,这四种场景均是通过如图1.38所示已经扫描完栈空间且被可达对象被标记为黑色作为出发点。四种场景分表是“堆删除引用,成为栈下游”、“ 栈删除引用,成为栈下游”、“ 堆删除引用,成为堆下游”、“ 栈删除引用,成为堆下游”。
2. 场景一:堆删除引用,成为栈下游
场景一主要描述的是对象被堆对象删除引用,成为栈对象的下游情况,伪代码如下:
//伪代码说明:
//例如: 堆对象4->对象7 = 对象7,含义是:对象7 被 对象4引用
栈对象1->对象7 = 堆对象7; //将堆对象7 挂在 栈对象1 下游
堆对象4->对象7 = null; //对象4 删除引用 对象7
现在我们执行上述场景代码中的第一条代码逻辑:“ 栈对象 1-> 对象 7 = 堆对象 7”,将白色的对象7添加到黑色的对象1下游,这里要注意的是,因为栈不启动写屏障机制,所以白色的对象7将直接挂在黑色的对象1下面,且对象7的颜色依然是白色。现在我们扫描到对象4,那么对象4被标记成了灰色,如图1.39所示。
图1.39 混合写屏障场景1-1
然后我们执行上述场景代码中的第二条代码逻辑:“堆对象 4-> 对象 7 = null”,灰色的对象4删除白色的对象7(删除即新赋值为null)。这里因为对象4所处在堆空间范围,所以会触发写屏障,那么被删除的对象7将被标记为灰色,如图1.40所示。
图1.40 混合写屏障场景1-2
所以通过场景一的情况来看,对象7最终被挂在了对象1的下游。由于对象7是灰色的,所以不会被当做垃圾进行回收,这样就保护了起来。场景一的混合写屏障中,也不会再次给栈空间的对象启动STW,再重新扫描一遍。那么接下来的过程就依旧遵循混合写屏障的三色标记法逻辑进行处理,最终对象4和对象7均会被标记为黑色,GC最终会回收掉对象5、对象8和4象6。
3. 场景二:栈删除引用,成为栈下游
场景二主要描述的是对象被一个栈对象删除引用,成为另一个栈对象的下游情况,伪代码如下:
new 栈对象9; //在栈空间新建一个对象9
对象9->对象3 = 对象3; //将栈对象3 挂在 栈对象9 下游
对象2->对象3 = null; //对象2 删除引用 对象3
现在我们执行上述场景代码中的第一条代码逻辑:“new 栈对象9”,根据混合写屏障的限定条件,任何在栈范围上新创建的内存对象均会被标记为黑色。这里我们继承上述图1.38所描述的内存布局场景,所以对象9目前是被程序根节点集合引用的黑色对象,如图1.41所示。
图1.41 混合写屏障场景2-1
然后我们执行上述场景代码中的第二条代码逻辑:“对象9->对象3 = 对象3”,对象9添加下游对象3,因为对象9是栈范围空间,所以添加过程并不会触发写屏障,直接将对象3挂在对象9的下面即可,如图1.42所示。
图1.42 混合写屏障场景2-2
最后我们执行上述场景代码中的第三条代码逻辑:“对象2->对象3 = null”,对象2将删除下游对象3。由于对象2属于栈范围内,所以依然不触发写屏障机制,对象2将直接将对象3从下游摘除,如图1.43所示。
图1.43 混合写屏障场景2-3
所以我们通过上述的过程看到,在混合写屏障的机制中,一个对象从一个栈对象下游转移到另一个对象的下游,由于栈对象均为黑色,所以不必要启动写屏障和STW机制就能够保证对象的安全性,这也是混合写屏障的巧妙设4之处。
4. 场景三:堆删除引用,成为堆下游
场景三主要描述的是对象被一个堆对象删除引用,成为另一个堆对象下游的情况,伪代码如下:
堆对象10->对象7 = 堆对象7; //将堆对象7 挂在 堆对象10 下游
堆对象4->对象7 = null; //对象4 删除引用 对象7
我们现在复原场景三的内存布局为,现在我们在堆空间范围内有一个黑色对象10(不为黑色也无所谓,因为对象10是堆空间可达对象,最终它都会被标记为黑色),我们这里不考虑对象10可能是其他颜色,因为黑色比较特殊,如果对象10为白色,那么对象10的下游毕竟会被扫描到,也就是安全的,同理如果对象10是灰色,那么对象10的下游也是安全的。只有对象10为黑色的时候,才会有下游内存不安全的情况。所以目前的内存布局如图1.44所示。
图1.44 混合写屏障场景3-1
现在我们执行上述场景代码中的第一条代码逻辑:“ 堆对象 10-> 对象 7 = 堆对象 7”,堆对象10添加下游引用白色的堆对象7。由于对象10是在堆空间范围内,这里的写操作将触发屏障机制,根据混合写屏障的限定条件,被添加的对象将被标记为灰色,那么白色的对象7将被标记灰色,这样同时也间接的保护了白色的对象6,如图1.45所示。
图1.45 混合写屏障场景3-2
然后我们执行上述场景代码中的第二条代码逻辑:“堆对象 4->对象 7 = null”,灰色的堆对象4删除下游引用堆对象7。由于对象4所在堆空间范围内,所以触发屏障机制,根据混合写屏障的限定条件,被删除的对象将被标记为灰色,那么将标记对象7为灰色(虽然对象7已经是灰色),如图1.46所示。
图1.46 混合写屏障场景3-3
通过上述的几个过程,原本白色的对象7已经成功的从一个堆对象4的下面转移到一个黑色的堆对象10下面,并且对象7以及它的下游对象(对象6)均被保护起来,而整体过程中也没有用到STW来耽误程序的运4性能。
5. 场景四:栈删除引用,成为堆下游
场景四主要描述的是对象从一个栈对象被删除引用,成为另一个堆对象的下游情况,伪代码如下:
栈对象1->对象2 = null;//将栈对象1的下游 对象2删除
堆对象4->对象7 = 栈对象2;//对象4 删除引用 对象7,同时引用新下游对象2
这里我们依然延续图6.37的内存布局场景。
这里我们依然延续图1.38的内存布局场景。
现在我们执行上述场景代码中的第一条代码逻辑:“栈对象1->对象2 = null”,栈对象1删除栈对象2的引用。由于对象1属于栈空间范围,所以不触发写屏障机制,那么对象1将直接删除对象2以及对象2所关联的全部下游对象,如图1.47所示。
图1.47 混合写屏障场景4-1
然后我们执行上述场景代码中的第二条代码逻辑:“堆对象4->对象7 = 栈对象2”,这条代码实际上是执行了两个动作。
第一个是将堆对象4之前的下游白色对象7删除掉。
第二个是将堆对象4的新下游对象添加为栈对象2,如图1.48所示。
图1.48 混合写屏障场景4-2
在对象4在执行上述两个动作的时候,由于对象4在堆空间范围内,将触发写屏障机制,根据混合写屏障的限定条件,被删除的对象将被标记为灰色,新添加的对象也会被标记为灰色。所以对象7被标记成灰色对象,这样对象7的下游对象6就得到了保护。对象2是新添加,那么对象2也将执行标记灰色的过程,这里由于对象2已经是黑色,属于安全的,那么对象2将继续保持黑色,如图1.49所示。
图1.49 混合写屏障场景4-3
最终我们成功的改变了一个本来是被栈引用的对象2挂在了堆对象4的下游。而依然保持内存的依赖关系和安全状态。那么之后会通过几次循环遍历,对象1、对象4、对象2、对象3均会被标记为黑色,而对象7和对象6会在本轮GC中也被标记为黑色。本轮GC最后回收的白色内存是对象5、对象8和对象11。
但这里有个疑问,对象7和对象6已经是和程序的Root Set断开了,为什么确没有被回收?这就是混合写屏障的延迟问题,在一定概率情况,为了去掉STW会有一些内存会延迟一周期被回收掉。等到第二轮GC,对象7和对象6如果没有外界添加,他们终将会成为白色垃圾内存而4回收。
五
总结
本章介绍了Golang中内存GC垃圾回收机制的演进迭代。发现GC的回收机制是一直在不断的优化,其目的是为了提高GC回收的性能。目前Golang的垃圾回收性能已经是非常的出色了,本章节只是介绍到了Go的1.8版本,Go的之后版本也有很多性能上的优化,但是几次变革较大的地方是本章要介绍了。
垃圾回收目前是三色标记加上屏障机制,影响垃圾回收性能的就是STW(Stop The World)机制,为了保护内存的安全性,不得不有STW,但是混合写屏障机制几乎可以完全不用STW来进行并行的垃圾回收,程序并不需要暂定就可以动态的清洗程序中的内存。
本章希望读者记住Golang中GC演进的几次里程碑,他们各自也都不是十分完美的解决方案,但通过对比我们可以得出 GoV1.3普通标记清除法,整体过程需要启动STW,效率极低。GoV1.5的三色标记加插入写屏障或删除写屏障方法,堆空间启动写屏障,栈空间不启动,全部扫描之后,需要重新扫描一次栈(需要STW),效率普通。GoV1.8三色标记法加混合写屏障机制,栈空间不启动屏障机制,堆空间启动屏障机制。整个过程几乎不需要STW,效率较高。
往
期
推
荐
致力于互联网教育技术的创新和推广
扫码关注我们 | @学而思网校技术团队