图解Go1.5屏障机制和Go1.8混合屏障

0 阅读15分钟

强-弱三色不变式:

1.强三色不变式:

不存在黑色对象引用到白色对象的指针,如图所示.

强三色不变色实际上是强制性的不允许黑色对象引用白色对象.这样就不会出现白色对象被误删的情况.

2.弱三色不变式:

所有被黑色对象引用的白色对象都处于灰色保护状态.如图所示.

弱三色不变式强调.黑色对象可以引用白色对象.但是这个白色对象必须存在其它灰色对象对它的引用.或者可达它的链路上游存在灰色对象.这样实则是黑色对象引用白色对象.白色对象处于一个危险被删除的状态.但是上游灰色对象的引用.可以保护白色对象使其安全.

插入屏障:

插入屏障的具体操作是.在A对象引用B对象的时候.B对象被标记为灰色(将B挂在A下游.B必须被标记为灰色).

插入屏障实际上是满足强三色不变式(不存在黑色对象引用白色对象的情况.因为白色会强制变成灰色).插入屏障伪代码.

添加下游对象(当前下游对象slot.新下游对象ptr){

//第一步

标记灰色(新下游对象ptr)

//第二步

当前下游对象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没有引用任何对象.也没有被任何对象引用.这些内存对象全部被标记为白色.在白色标记表中将全部的对象装入其中.如图所示.

依据三色标记的流程.遍历Root Set根节点集合.非递归形式.只遍历一次.能够标记出第一层的灰色节点对象1和对象4.同时这些灰色节点也被添加至灰色标记表中.如图所示.

按照三色标记法的顺序来讲解.接下来就遍历灰色标记表中的对象1和对象4.将可达的对象从白色标记为灰色.同时被遍历的灰色对象被标记为黑色.如图所示.

由于并发的特性此刻外界向已经标记为黑色的对象4添加白色的对象8.向已经标记位黑色的对象1添加下游白色的对象9.如图所示,

对象1是栈空间.根据插入屏障的特点.为了保护性能.栈空间创建对象不触发插入屏障.但是对象4在堆空间.此时对象4将触发插入屏障机制.

由于插入写屏障的机制(黑色对象添加白色对象.所以白色对象改为灰色).所以当堆上的对象4添加对象8的时候.对象8将被标记为灰色.而对象9依然是白色.如图所示.

之后就是正常的三色标记流程.继续循环上述的流程.直到没有灰色节点.目前得到的对象状态如图所示.

栈空间的对象1 对象2 对象3被标记为黑色.堆空间上的对象4 对象7 对象8被标记为黑色.其它对象依然是白色.

这个时候插入屏障并不会立刻执行垃圾回收动作.而是会做一个额外的扫描.但是如果栈不添加.当全部三色标记扫描后.栈上有可能依然存在白色对象引用的情况.如上图对象9.所以要对栈重新进行三色标记扫描.这次为了对象不丢失.这次扫描要启动STW暂停.直到栈空间三色标记结束.

栈空间的全部内存对象均被重新标记为白色(对象1 对象2 对象3 对象9 对象5),而且会对这些白色对象启动STW.使程序暂停.以便将这些白色对象保护起来.所以对以上被保护的对象进行任何读写操作均会被拦截且阻塞.防止外界干扰(如有新的白色对象被黑色对象添加).与此同时,对于其它堆空间的对象将不会触发STW.这样也是为了保证堆空间的GC回收性能,如图所示,

接下来就是在STW保护的区域内.继续执行三色标记流程.直到全部可达白色对象都被扫描到.即没有灰色节点.最后得到一个最终的状态.如图所示.

当全部内存对象只有白色和黑色的时候.就会停止STW释放保护层.如图所示.

在最后的扫描之后的内存中就全部为黑色对象.如图所示.

删除屏障:

删除屏障的具体操作是.被删除的对象.如果自身为灰色或者白色.则被标记为灰色.删除屏障实际上是满足弱三色不变式.目的是保护灰色对象到白色对象的路径不会断.删除屏障伪代码如下.

添加下游对象(当前下游对象slot,新下游对象 ptr){

//第一步 

if(当前下游对象slot是灰色 || 当前下游对象slot是白色){

//slot 为被删除对象.标记为灰色.

标记灰色(当前下游对象slot)

}

//第二步 

当前下游对象slot = 新下游对象ptr 

}

删除屏障伪代码场景如下:

//A对象,删除B对象的引用.B被A删除.被标记为灰.(如果B之前为白)

添加下游对象(B,nil)

//A对象.更换下游B变成C.B被A删除.被标记为灰(如果B之前为白)

添加下游对象(B,C)

删除屏障是当一个对象的引用被摘掉的时候.或者当一个对象引用被上游替换的时候.该对象标记为灰色.标记为灰色的目的:被删除的白色对象.如果又被其它的黑色对象引用.则被删除的对象应该被回收掉.

如图所示.在开始执行删除屏障的三色标记之前.目前的内存情况如下.在栈空间有对象1引用对象5 对象5引用对象2 对象2引用对象3.对象3没有下游对象.在堆空间有对象4引用对象7.对象7没有下游对象.没有对象引用的对象6.也没有下游对象.

依然根据三色标记的流程.遍历Root Set根节点集合.非递归形式.只遍历一次.能够标记出第一层的灰色节点对象1和4.同时这些灰色节点也被添加至灰色标记表中.如图所示.

如果此时灰色对象1删除白色对象5.并且不触发删除屏障机制.则白色对象5连同下游对象2和对象3将与主链路断开.最终也会被清除.如图所示.

但是目前的三色标记法是删除屏障机制.依照算法.被删除的对象标记位灰色.目前是保护对象5和下游对象(为什么需要保护.如果不将对象5标记为灰色会出现哪些意外问题.如果对象1已经删除了对象5.对象5依旧是白色.那么由于整体流程没有添加STW保护.极有可能在删除的过程中.同一时刻又有一个已经被标记为黑色对象的引用了这个对象5.对象5依然是程序流程中需要依赖的合法内存对象.但是最终会按照白色对象被GC回收掉.因为黑色的下游对象没有被保护起来).将被标记成灰色.如图所示.

按照三色标记的顺序.接下来遍历灰色标记表中的对象1 对象4 对象5.将它们可达的对象从白色标记为灰色.同时遍历的灰色对象被标记为黑色.如图所示.

继续循环上述流程进行三色标记.直到没有灰色节点.最终的状态如图所示.

除了对象6全部节点均被标记为黑色.

最后执行回收清除流程.将白色对象全部通过GC回收处理.如图所示.

Go V1.8混合写屏障:

插入写屏障和删除写屏障虽然都可以在一定程度上解决STW带来的无法并行处理的问题.但是也都各有短板.

1).插入写屏障:结束时需要STW重新扫描栈.标记栈上引用的白色对象的存活.

2).删除写屏障:回收精度低.GC开始时STW扫描堆栈来记录初始快照.这个过程会保护开始时刻的所有存活对象.

1.8引入了混合写屏障机制.避免了对栈的重新扫描过程.这也极大的减少了STW的时间.同时结合了插入写屏障和删除写屏障两者的优点.

混合写屏障规则:

混合写屏障的具体操作一般需要如下几个条件限制.

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没有上游对象.且引用对象8.对象8下游没有引用对象.堆空间范围的Root Set根节点集合引用对象4.对象4引用对象7.对象7下游没有被引用对象.对象6没有上游对象.也没有下游对象.这些内存对象均被标记为白色.

现在GC开始.按照上述混合写屏障的几个步骤.它的第一步就是扫描栈区.将可达对象全部标记为黑色.所以扫描栈区结束的时候.对象1 2 3均可达.被标记成黑色.如图所示.

场景1:堆删除引用,成为栈下游:

伪代码说明

堆对象4->对象7=对象7    对象7被对象4引用.

栈对象1->对象7=堆对象7;             //将堆对象7挂在栈对象1下游

堆对象4->对象7=null;                   //对象4删除引用对象7

将白色对象7添加到黑色对象1的下游.需要注意的是.因为栈不启动写屏障机制.所以白色对象7将直接挂在黑色对象1下面.并且对象7的颜色依旧是白色.现在扫描到对象4.此时对象4被标记成灰色.如图所示.

第二条逻辑.因为对象4在堆上.所以会触发写屏障.被删除的对象7会被标记为灰色.如图所示.

对象7最终被挂在了对象1的下游.由于对象7是灰色的.所以不会被当做垃圾进行回收.这样就保护起来了.在场景1的混合写屏障中.也不会再次给栈空间对象启动STW.再重新扫描一遍.接下来的过程就是依旧遵循混合写屏障的三色标记逻辑进行处理.最终对象4和7均会被标记为黑色.GC最终会回收对象5 8 6.

场景2:栈删除引用,成为栈下游:

伪代码:

new 栈对象9        //栈空间新建一个对象9

对象9->对象3=对象3      //将栈对象3挂在栈对象9下游.

对象2->对象3=null        //对象2删除引用对象3

执行第一条语句.new栈对象9.根据混合写屏障的限定条件.任何在栈范围上新创建的内存对象均会被标记为黑色.如图所示.

然后执行第二条语句.因为对象9是栈空间范围.所以添加过程不会触发写屏障.直接将对象3挂在对象9下面.如图所示.

最后执行第三天语句.对象2属于栈范围内.所以依然不触发写屏障机制.对象2直接将对象3从下游移除.如图所示.

在混合写屏障机制中.一个对象从一个栈对象下游转移到另一个对象的下游.由于栈对象均为黑色.所以不必启动写屏障和STW机制就能保证对象的安全性.

场景3:堆删除引用.成为堆下游

伪代码:

堆对象10->对象7=堆对象7;    //将堆对象7挂在对象10下游.

堆对象4->对象7=null;     //对象4删除引用对象7.

内存布局情况如图.

执行第一条语句.堆对象10在堆空间范围内.这里的写操作将触发屏障机制.根据混合写屏障的限定条件.被添加的对象将被标记为灰色.所以白色的对象7将被标记为灰色.这样间接保护了对象6.如图所示.

执行第二条语句.由于对象4在堆空间范围内.所以触发屏障机制.根据混合写屏障的限定条件.被删除的对象将被标记为灰色.所以将对象7标记为灰色.(对象7已经是灰色).如图所示.

原本白色的对象7已经成功的从一个堆对象4的下面转移到一个黑色的堆对象10下面.并且对象7及它的下游对象均被保护起来.整个过程中没有用到STW.

场景4:栈删除引用.成为堆下游

伪代码:

栈对象1->对象2=null;        //将栈对象1的下游对象2删除.

堆对象4->对象7=栈对象2;   //对象4删除引用对象7.同时引用新下游对象2.

执行第一条语句.对象1属于栈空间范围.所以不触发写屏障机制.此时对象1直接删除对象2所关联的全部下游对象.如图所示.

执行第二条语句.执行了两个动作.

第一个是将对堆对象4之前的下游白色对象7删除.

第二个是将堆对象4的新下游对象添加为栈对象2.如图所示.

当对象4执行上述两个动作的时候.由于对象4在堆空间范围内.将触发写屏障机制.根据混合写屏障的限定条件.被删除的对象将被标记为灰色.新添加的对象也会被标记为灰色.所以对象7被标记为灰色对象.这样对象7的下游对象6就得到了保护.对象2是新添加的对象.那么对象2也将执行标记灰色的过程.这里由于对象2已经是黑色.属于安全对象.那么对象2继续保持黑色.如图所示.

最终成功的改变了一个本来是被栈引用的对象2挂在了堆对象4的下游.而依然保持内存的依赖关系和安全状态.之后会通过几次循环遍历.对象1 4 2 3均会被标记为黑色.而对象6 7会在本历年GC中也被标记为黑色.最后回收白色内存5 8.

金鸭消香,银虬泻水,谁家夜笛飞声。正上林雪霁,鸳鸯晶莹。鱼龙舞罢香车杳,剩尊前、袖掩吴绫。狂游似梦,而今空记,密约烧灯。 
追念往事难凭。叹火树星桥,回首飘零。但九逵烟月,依旧笼明。楚天一带惊烽火,问今宵、可照江城。小窗残酒,阑珊灯灺,别自关情。  纳兰

语雀地址www.yuque.com/itbosunmian…?

《Go.》 密码:xbkk 欢迎大家访问.提意见.

如果大家喜欢我的分享的话.可以关注我的微信公众号

念何架构之路