当前位置: 首页 > 科技观察

说说Go的三色写法

时间:2023-03-19 14:43:45 科技观察

本文转载自微信公众号《跨界建筑师》,作者Zachary。转载本文请联系跨界架构师公众号。大家好,我是Z哥,今天带来一篇久违的技术文章。之前很多朋友会问,Z哥,你好久没发技术文章了。其实,主要原因有以下几点。第一,目前的工作重在业务和管理,在技术上的精力投入确实没有以前那么多了。这也限制了我在纯技术方面的知识输出。第二,虽然我在工作之余也投入了一部分精力在技术学习上,但大部分还是集中在对新技术、新框架的理解和熟悉上。涉及的知识层次比较浅,即使发表了对大家帮助也不大,所以就不发表了。第三,从长远来看,我不想把自己过多地局限在技术圈里。因为在我看来,技术只是一门手艺,是吃饭的家伙,但吃饭的家伙从来不只是技术,还有很多其他方面。甚至这些东西很多都没有具体的技术细节那么“标准化”,很多都是汗水积累下来的“非标准化”经验。我认为这些经验的价值不亚于技术知识。所以,作为立志要和大家交朋友的Z哥,自然不想把自己局限在“技术”这个小圈子里。好了,回到本文的主题。最近刚好在学习Golang,对里面用到的三色标记法的GC机制有点好奇(一开始是因为名字让我想起了三色杯冷饮~),所以稍微了解了一下,在这里分享出来,说不定对你以后的面试有帮助。1.判断对象存活的思路在GC领域,判断对象存活的主流思路有两种,“引用计数”和“可达性分析”。01引用计数顾名思义,引用计数的思想就是对每个对象进行计数。每被另一个对象引用一次,计数就会+1,当引用失效时,计数就会-1。当计数器的值为0时,表示未使用,可以回收。02可达性分析可达性分析的思想是判断一个对象是否可以通过引用链接到达。如果可以到达,说明该对象当前正在被使用,无法回收;否则,未到达的对象被认为是不可到达的。用过的,可以回收。这个参考链接的结构类似于有向循环图,但是根节点不止一个,是一个叫做GCRoots的集合。目前主流的GC机制大多采用“可达性分析”的路线。Go、Java、.Net等都是如此,为什么引用计数不好用呢?因为它有一个特别严重的问题:它不能处理循环引用。像上面这种情况,引用计数永远不会为0,这些对象永远不会被回收,会严重影响回收的效果。但这并非没有用。其回收效果实时性更好。它可以与“可达性分析”一起使用,以发挥各自的优势,在不同的场景下使用不同的策略。由于“可达性分析”的思想是主流,所以后来的很多回收算法都是基于这个思想,三色标记法就是其中之一。我们今天主要讲一下。在讲解二三色记法的具体原理之前,先了解一个概念,“StopTheWorld”,简称“STW”。垃圾收集器的工作流程大致如下:标记哪些对象是存活的,哪些是可回收的。回收(清除/复制/碎片整理)。如果在收集期间有对象被移动(复制/缩小),则还需要更新引用。标记的第一步可以分为两个步骤。标记GCROOT可以关联的对象。这里将是STW。从GCRoots的直接关联对象开始遍历整个对象图。这里不会有STW。垃圾回收算法主要是在第一步做第二步,三色标记法也不例外。它用以下三种颜色标记从GCRoots遍历出来的对象:白色、初始值。本次未扫描的物体默认为白色。确认不可达的对象也是白色的,但会被标记为“不可达”。灰色,中间状态。外部引用了该对象,但未完全检测到该对象引用的其他对象。黑色,此对象被其他对象引用,已检测到此对象引用的其他对象。其实这三种颜色并不重要,重要的是它们所表达的状态,灰色的中间状态,经过标记处理之后,就只会是白色或者黑色。在整个过程中,这些状态的变化如下图所示。看似完美的解决方案其实有一个问题:在标记过程中,对象引用发生了变化。会导致“多标签”和“漏标签”两个问题。多标如下图:由于第2步不会STW,所以可能存在扫描A标记为黑色后,重新引用一个原本标记为白色的D(C断开引用到D)。这时D会被回收,导致程序出现意想不到的bug。“缺失”就是:对象E/F/G“应该”被回收。但是因为E已经变灰了,所以还是会认为是存活对象,继续遍历。最终的结果是:这部分对象仍然会被标记为alive,即本轮GC不会回收这部分内存。解决这两个问题的传统方法有两种:在断开引用时做额外的处理。当“黑色”对象重新创建对“白色”对象的引用时,做额外的处理。(回收开始后新创建的对象默认为黑色)。第一个idea的专业名称是“writebarrier”,第二个是“readbarrier”。其实这个名字只是一个噱头。你可以把它们理解为我们在编程中经常使用的AOP概念,在修改和阅读之前做一些操作。基于“writebarrier”,可以扩展两种解决方案:IncrementalUpdate。对于新添加的引用,记录下来,等待回溯。这个操作是在“修改操作之后”执行的,这是JVM中CMS垃圾收集器的思想。原始快照(SnapshotAtTheBeginning,SATB)。当某个时刻的GCRoots确定了,此时的对象图就已经确定了。如果期间有变化,可以记录下来,保证标记还是按照原来的看法。这个操作是在“修改操作之前”进行的,JVM中的G1垃圾回收器就是采用了这个思路。理论上,使用“RememberedSet”,SATB的效率比增量更新要高,但是会消耗更多的内存。基于“读屏障”的解决方案是:在“黑”对象重新建立“白”对象的引用之前,记录白对象,避免被回收。这个动作是在“读取操作之前”执行的,JVM中的ZGC垃圾收集器就是这个想法。在Golang中(1.8版本之后),使用了一种新的机制,称为“混合写屏障”机制。它的思想总结起来就是4句话:把对象分为堆上的对象和栈上的对象。GC开始扫描并将堆栈上的所有对象标记为黑色,不需要STW。并且之后不会有第二次重新扫描。在GC期间,在堆栈上创建的任何新对象都是黑色的。在GC期间删除或添加到堆中的对象标记为灰色。稍后继续扫描。你看,这些原理其实并没有那么复杂。相信只要弄清楚自己面临的是什么问题,也能想到这些解决方案。好的,总结一下。在这篇文章中,Z哥和大家分享了我对Golang中GC机制“三色标记法”的理解。GC底层判断对象存活的方式主要有两种,引用计数和可达性分析。由于引用计数中的循环引用问题,大部分GC都是按照后一种思路实现的,Golang也不例外。“三色标记法”的原理是将物体分为三种状态:白色,默认值。此藏品未扫描之物件均为白色。确认不可达的对象也是白色的,只是标记为“不可达”。灰色,中间状态。外部引用了该对象,但未完全检测到该对象引用的其他对象。黑色,此对象被其他对象引用,已检测到此对象引用的其他对象。最后回收白色状态的对象。为了解决标签缺失和标签多的问题,它通过“混合写屏障”的机制来解决。思路是将对象分为堆上的对象和栈上的对象。GC开始扫描并将堆栈上的所有对象标记为黑色,不需要STW。并且之后不会有第二次重新扫描。在GC期间,在堆栈上创建的任何新对象都是黑色的。在GC期间删除或添加到堆中的对象标记为灰色。稍后继续扫描。希望对你有帮助。