强-弱三色不变式:
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 欢迎大家访问.提意见.
如果大家喜欢我的分享的话.可以关注我的微信公众号
念何架构之路