Golang的垃圾回收机制

单刀直入只聊golang的垃圾回收机制
go语言垃圾回收总体采用的是经典的mark and sweep算法—三色标记清除法(tricoloer mark and sweep)
垃圾回收的目标就是把那些已经分配的但没有对象引用的找出来并回收掉,以供后续内存分配时使用。
垃圾回收开始时从root对象开始扫描,把root对象引用的内存标记为“被引用”,考虑到内存块中存放的可能是指针,所以还需要递归的进行标记,未被标记的全部标识为未分配即完成了回收。
内存标记(Mark)
golang的里面的内存由span、bitmap、arena三部分组成,其中span维护了一个个内存块,并由一个位图allocBits表示每个内存块的分配情况。在span数据结构中还有另一个位图gcmarkbits用于标记内存块被引用的情况。
如上图所示,allocBits记录了每块内存分配情况,而gcmarkBits记录了每块内存标记情况。标记阶段对每块内存进行标记,有对象引用的的内存标记为1(如图中灰色所示),没有引用到的保持默认为0.
allocBits和gcmarkBits数据结构是完全一样的,标记结束就是内存回收,回收时将allocBits指向gcmarkBits,则代表标记过的才是存活的,gcmarkBits则会在下次标记时重新分配内存,非常的巧妙
三色标记法
标记的对象需要一个标记队列来存放,可以简单想象成把对象从标记队列中取出,将对象的引用状态标记在span的gcmarkBits,把对象引用到的其他对象再放入队列中。
三色只是为了叙述上方便抽象出来的一种说法,实际上对象并没有颜色之分;这里的三色,对应了垃圾回收过程中对象的三种状态:
灰色:对象还在标记队列中等待
黑色:对象已被标记,gcmarkBits对应的位为1
白色:对象未被标记,gcmarkbits对应的位为0
举栗说明:
root对象包括全局变量,各个G stack上的变量等。
根对象在垃圾回收的术语中又叫做根集合,它是垃圾回收器在标记过程时最先检查的对象,包括:
  1. 全局变量:程序在编译期就能确定的那些存在于程序整个生命周期的变量。
  2. 执行栈:每个 goroutine 都包含自己的执行栈,这些执行栈上包含栈上的变量及指向分配的堆内存区块的指针。
  3. 寄存器:寄存器的值可能表示一个指针,参与计算的这些指针可能指向某些赋值器分配的堆内存区块。

当前内存中有A~F一共6个对象,根对象a,b本身为栈上分配的局部变量,根对象a、b分别引用了对象A、B, 而B对象又引用了对象D,则GC开始前各对象的状态如下图所示:
初始状态下所有对象都是白色的。灰色对象和黑色对象的都为空。这里root对象a引用了A对象,b引用了对象B,而B又引用了对象D;
接着开始扫描根对象a、b:
由于根对象引用了对象A、B那么A、B变为灰色对象,接下来就开始分析灰色对象,分析A时,A没有引用其他对象就很快转入黑色,B引用了D,B转入黑色的同时需要将D转为灰色。
由于D没有引用其他对象,所以D转入黑色,标记过程结束。最终黑色的对象会被保留下来,白色对象会被回收掉。这里为什么不直接把D置为黑色呢?因为在遍历跟对象的时候,不递归遍历,所以需要在灰色的节点再遍历一遍。这里如果灰色的还有引用,就会继续把引用的对象放入灰色集合,而灰色对象本身变为黑色对象。
那如果GC期间检测到对象有变化怎么办?
假设一个场景:
被标记为灰色对象A有指针q指向一个白色对象W,在还没有扫描到A,已经标记为黑色的对象B,创建指针p指向白色对象W,恰好此时指针q移除,白色对象W就被挂在了已经扫描完成的黑色对象B下面,正常来说白色对象W应该先灰然后再黑,但是B已经扫描完成,不再扫描了,就等这被回收了。这样白色对象W就会被当做是白色垃圾了。 如果他还有很多下游对象的话, 也会一并都清理掉。(WTF!!)但是W确实还被B引用着,这就出现了对象丢失。
为了防止这种现象产生,就是最简单粗暴的方式STW(stop the world)magic;
想象一个万物静止的画面~~就像火影的无限月读一样。
STW过程中 CPU不执行用户代码,全部用于垃圾回收。直接禁止掉其他用户程序对对象引用关系的干扰,但是STW的过程有明显的资源浪费,对所有的用户程序都有很大影响。MMP,咋那么多事呢。
屏障机制:巴鲁托洛米奥屏障果实
1 强弱三色不变
强制性不允许黑色对象引用白色对象的指针。
2 弱三色不变式
所有被黑色对象引用的白色对象都处于灰色保护状态;黑色对象可以引用白色对象,白色对象存在其他灰色对象对它的引用,或者可达它的链路上游存在灰色对象
为了达到上述解决方案,通过2种屏障
a 写屏障(write-barrier)
b 删除屏障
A 写屏障
在A对象引用B对象的时候,B对象被标记为灰色。就不存在黑对象直接引用白对象。黑色对象的内存槽有2种位置:栈和堆。栈空间的特点是容量小,但是要求相应速度快。
所以写屏障机制在栈空间的对象操作中不使用,而仅仅使用在堆空间对象的操作中。
举栗:
对象 8和9 是在由于并发,外界添加的对象,对象4在堆区,即将出发屏障机制,对象1不触发。由于写屏障的原因,对象8会变成灰色,而对象9依然是白色。之后仍旧要对栈区的从新进行三色标记扫描,但这次为了对象不丢失, 要对本次标记扫描启动STW暂停,为了防止外界干扰。直到栈空间的三色标记结束。最后将栈和堆空间 扫描剩余的全部 白色节点清除. 这次STW大约的时间在10~100ms间.
B 删除屏障
 被删除的对象,如果自身为灰色或者白色,那么被标记为灰色
删除屏障步骤:
1 全部标记为白色对象,将所有对象放入白色集合中
2 遍历根节点,得到灰色节点
3 此时有对象A引用对象B->C->D,A断开了对B的引用。如果不触发删除写屏障,BCD与主链路断开,最后均会被清除。
4 触发写屏障,被删除的对象B,自身被标记为灰色,遍历灰色标记表,将可达对象,从白色标记为灰色,遍历之后灰色标记为黑色。
5 继续循环上述流程进行三色标记,直到没有灰色节点
6 清除白色
写屏障和删除屏障的短板:
插入写屏障:结束时需要STW来重新扫描栈,标记栈上引用的白色对象的存活
删除写屏障:回收精度低,GC开始时STW扫描堆栈来记录初始快照,这个过程会保护开始时刻的所有存活对象
针对此Go V1.8版本引入了混合写屏障机制(hybrid write barrier)避免了对栈re-scan的过程,极大的减少了STW的时间。结合了两者的优点。
1) 混合写屏障规则
具体操作:
1、GC开始将栈上的对象全部扫描并标记为黑色(之后不再进行第二次重复扫描,无需STW),
2、GC期间,任何在栈上创建的新对象,均为黑色。
3、被删除的对象标记为灰色。
4、被添加的对象标记为灰色。
满足: 变形的弱三色不变式.
这里我们注意, 屏障技术是不在栈上应用的,因为要保证栈的运行效率。
分别说一下在栈上删除、添加和在堆上删除、添加
栈区有对象A引用了堆区的对象C,堆区也有对象B引用了对象C,如果对象B删除了对象C的引用关系,因为对象B是堆区,除以触发写屏障,标记被删除对象C为灰色。
1 在栈上创建一个对象A,混合写屏障模式,GC过程中任何新创建的对象均标记为黑色。
2 栈对象A添加下游引用栈对象D(直接添加,栈不启动屏障,无屏障效果)
3 栈对象B->C->D ,对象B删除对象D的引用关系(直接删除,栈不启动写屏障,无屏障效果)
堆对象添加、删除
1 堆对象A已经被设置为黑色,堆对象A添加下游引用堆对象C,触发屏障保护机制,被添加的对象标记为灰色,对象C变成灰色,而对象C的下游对象被保护。
2 此时堆对象B(原本B->C->D调用链,都在堆区),删除下游引用的对象C,触发屏障机制,被删除的对象标记为灰色,C被标记为灰色。
从一个栈对象被删除,成为另一个堆对象的下游:
栈对象A删除对栈对象B的引用(栈对象不触发写屏障),堆对象D引用了栈对象B,堆对象在删除的时候,触发写屏障,标记被删除对象E为灰色,保护E及下游节点。
Golang中的混合写屏障满足弱三色不变式,结合删除写屏障和插入写屏障的优点,只需要在开始时并发扫描各个goroutine的栈,使其变黑并一直保持,这个过程不需要STW,而标记结束后,因为栈在扫描后始终是黑色的,也无需再进行re-scan操作了,减少了STW的时间。
触发GC的机制:
1.    在申请内存的时候,检查当前当前已分配的内存是否大于上次GC后的内存的2倍,若是则触发(主GC线程为当前M)有点类似redis里面那个持久化aof重写。
2 监控线程发现上次GC的时间已经超过两分钟了,触发
3 手动触发,runtime.GC() 主要用于GC性能测试和统计
4 gcTriggerAlways: 强制触发GC
如何加速GC:
GC的分析工具:
  • go tool pprof
  • go tool trace
  • go build -gcflags=”-m”
  • GODEBUG=”gctrace=1”
GC中STW的衍变:
1.3 版本 STW,进行标记,清理垃圾,start the world
1.4 三色标记法,并发标记,并发Sweep,非分代,非移动,并发的收集器;即sweep和用户逻辑可以并发。即 STW–> MARK—-start the world & sweep
1.5 引入写屏障,先做一次很短暂的STW,做一些简单的状态处理,接下来对内存进行扫描,这个时候用户逻辑也可以执行。然后如果写屏障发起了信号,垃圾回收会捕捉到这个信号,然后重新扫描这个写屏障保护的内存。
stw–>用户线程—stw–> 清理垃圾
1.8 混合屏障,消除了stw中的重新扫描栈。
总结:golang的垃圾回收三色标记法的过程,以及产生的问题和屏障机制的引入。
golang的垃圾回收三色标记步骤:1 初始为全部白色对象
2 遍历根对象变成灰色对象,之后把根对象变成黑色,然后把其引用的对象也变成灰色。
3 遍历灰色的节点,变成黑色
4 清理剩下的白色节点
golang GC的发展过程:
Go 1:串行三色标记清扫
  • Go 1.3:并行清扫,标记过程需要 STW,停顿时间在约几百毫秒
  • Go 1.5:并发标记清扫,停顿时间在一百毫秒以内
  • Go 1.6:使用 bitmap 来记录回收内存的位置,大幅优化垃圾回收器自身消耗的内存,停顿时间在十毫秒以内
  • Go 1.7:停顿时间控制在两毫秒以内
  • Go 1.8:混合写屏障,停顿时间在半个毫秒左右
  • Go 1.9:彻底移除了栈的重扫描过程
  • Go 1.12:整合了两个阶段的 Mark Termination,但引入了一个严重的 GC Bug 至今未修(见问题 20),尚无该 Bug 对 GC 性能影响的报告
  • Go 1.13:着手解决向操作系统归还内存的,提出了新的 Scavenger
  • Go 1.14:替代了仅存活了一个版本的 scavenger,全新的页分配器,优化分配内存过程的速率与现有的扩展性问题,并引入了异步抢占,解决了由于密集循环导致的 STW 时间过长的问题

发表评论

电子邮件地址不会被公开。 必填项已用*标注