Golang底层原理剖析之垃圾回收GC

前置知识

概念了解

Root set根节点就是发现堆内存可达数据的一组起点,一般为bss段、数据段以及协程栈对应的元数据

GC用来清理堆,为什么还要扫描栈?因为栈上的对象会引用堆上的对象

Go V1.3-标记清除法

  • 标记(Mark phase)
  • 清除(Sweep phase)
    在这里插入图片描述

步骤

  1. 暂停程序的正常执行流程(开始STW)
  2. 从Root Set根节点出发找出所有可达对象
  3. 给所有可达对象坐上标记
  4. 开始清除未标记的不可达对象
  5. 恢复程序的正常执行流程(停止STW)

缺点

  1. STW,stop the world;让程序暂停,程序出现卡顿 (重要问题)
  2. 标记需要扫描整个heap
  3. 清除数据会产生heap碎片

Go V1.5-三色标记法

三色标记法实际上就是通过三个阶段的标记来确定清楚的对象都有哪些

步骤

  1. 暂停程序的正常执行流程(开始STW)
  2. 只要是新创建的对象,默认为白色
  3. GC开始时,从根节点开始遍历所有对象,把遍历到的第一层对象从白色变为灰色
  4. 遍历一次灰色集合,将灰色对象引用的对象变为灰色,把原来灰色对象变为黑色
  5. 重复第3步骤,直到没有灰色对象
  6. 回收所有白色对象,白色对象没有被任何引用,也就是垃圾
  7. 恢复程序的正常执行流程(停止STW)

图示

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

去除STW的三色标记法的图示

不难看出,我们上面已经清楚的体现三色的特性。但是这里面可能会有很多并发流程均会被扫描,执行并发流程的内存可能相互依赖,为了在GC过程中保证数据的安全,我们在开始三色标记之前就会加上STW,在扫描确定黑白对象之后再放开STW。但是很明显这样的GC扫描的性能实在是太低了。

那么Go是如何解决标记-清除(mark and sweep)算法中的卡顿(stw,stop the world)问题的呢?

我们加入如果没有STW,那么也就不会再存在性能上的问题,那么接下来我们假设如果三色标记法不加入STW会发生什么事情?

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

去除STW的三色标记法产生的问题

最后我们才发现,本来是对象4合法引用的对象3,却被GC给“误杀”回收掉了。

可以看出,有两种情况,在三色标记法中,是不希望被发生的。

  1. 一个白色对象被黑色对象引用(白色被挂在黑色下)
  2. 灰色对象与它之间的可达的白色对象遭到破坏(灰色同时丢了该白色)

==如果当以上两个条件同时满足时,就会出现对象丢失现象! ==

并且,如图所示的场景中,如果示例中的白色对象3还有很多下游对象的话, 也会一并都清理掉。

为了防止这种现象的发生,最简单的方式就是STW,直接禁止掉其他用户程序对对象引用关系的干扰,但是STW的过程有明显的资源浪费,对所有的用户程序都有很大影响。那么是否可以在保证对象不丢失的情况下合理的尽可能的提高GC效率,减少STW时间呢?答案是可以的,我们只要使用一种机制,尝试去破坏上面的两个必要条件就可以了。

Go V1.8-三色标记+混合写屏障

“强-弱” 三色不变式

我们让GC回收器,满足下面两种情况之一时,即可保对象不丢失。 这两种方式就是“强三色不变式”和“ 弱三色不变式”

强三色不变式

强三色不变式:强制性的不允许黑色对象引用白色对象(不存在黑色对象引用到白色对象的指针)
这样就不会出现有白色对象被误删的情况
在这里插入图片描述

弱三色不变式

弱三色不变式:黑色对象可以引用白色对象,但是这个白色对象必须存在其他灰色对象对它的引用,或者可达它的链路上游存在灰色对象
所有被黑色对象引用的白色对象都处于灰色保护状态
在这里插入图片描述

两种屏障方式

为了遵循上述的两个方式,GC算法演进到两种屏障方式,分别为“插入写屏障”, “删除写屏障”。

使用了插入写屏障或者删除写屏障的三色标记法,可以大幅减少STW的时间。

插入写屏障

介绍

行为->插入写屏障:在A对象引用B对象的时候,B对象被标记为灰色。(将B挂在A下游,B必须被标记为灰色)

满足->强三色不变式. (不存在黑色对象引用白色对象的情况了, 因为白色会强制变成灰色)

注:栈空间的特点是容量小,但是要求相应速度快,因为函数调用弹出频繁使用, 所以“插入写屏障”机制,对于栈中的对象是不生效的,“插入写屏障” 仅仅使用在堆中生效。所以在结束时需要STW来重新扫描栈,执行三色标记法回收白色垃圾,这次STW大约的时间在10~100ms间。

插入写屏障不对栈生效!插入写屏障不对栈生效!插入写屏障不对栈生效!

图示

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
当全部三色标记扫描之后,栈上有可能依然存在白色对象被引用的情况(如上图的对象9). 所以要对栈重新进行三色标记扫描, 但这次为了对象不丢失, 要对本次标记扫描启动STW暂停. 直到栈空间的三色标记结束.
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
最后将栈和堆空间 扫描剩余的全部白色节点清除. 这次STW大约的时间在10~100ms间

删除写屏障

介绍

行为->被删除的对象,如果自身为灰色或者白色,那么被标记为灰色
满足->弱三色不变式. (保护灰色对象到白色对象的路径不会断)

这种方式的回收精度低,一个对象即使被删除了最后一个指向它的指针也依旧可以活过这一轮,在下一轮GC中被清理掉。

堆栈皆可用弱三色不变式!

图示

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

两种屏障的缺点

  • 插入写屏障:结束时需要STW来重新扫描栈,标记栈上引用的白色对象的存活;
  • 删除写屏障:回收精度低,GC开始时STW扫描堆栈来记录初始快照,这个过程会保护开始时刻的所有存活对象。

混合写屏障

Go V1.8版本引入了混合写屏障机制(hybrid write barrier),避免了对栈re-scan的过程,极大的减少了STW的时间。结合了两者的优点。

注意混合写屏障是GC的一种屏障机制,所以只是当程序执行GC的时候,才会触发这种机制

​Golang中的混合写屏障满足弱三色不变式,结合了删除写屏障和插入写屏障的优点,只需要在开始时并发扫描各个goroutine的栈,使其变黑并一直保持,这个过程不需要STW,而标记结束后,因为栈在扫描后始终是黑色的,也无需再进行re-scan操作了,减少了STW的时间。

步骤

  1. GC开始将栈上的对象全部扫描并标记为黑色(之后不再进行第二次重复扫描,无需STW)
  2. GC期间,任何在栈上创建的新对象,均为黑色
  3. 被删除的对象标记为灰色
  4. 被添加的对象标记为灰色

满足->变形的弱三色不变式

这里我们注意, 屏障技术(步骤三四)是不在栈上应用的,因为要保证栈的运行效率
这里我们注意, 屏障技术(步骤三四)是不在栈上应用的,因为要保证栈的运行效率
在这里插入图片描述
在这里插入图片描述

混合写屏障的场景分析

对象被堆对象删除引用,被栈对象引用

在这里插入图片描述
在这里插入图片描述

对象被栈对象删除引用,被另一个栈对象引用

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

对象被堆对象删除引用,被另一个堆对象引用

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

对象被栈对象删除引用,被堆对象引用

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

总结

​ 以上便是Golang的GC全部的标记-清除逻辑及场景演示全过程。

GoV1.3 - 普通标记清除法,整体过程需要启动STW,效率极低。

GoV1.5 - 三色标记法, 堆空间启动写屏障,栈空间不启动,全部扫描之后,需要重新扫描一次栈(需要STW),效率普通

GoV1.8 - 三色标记法+混合写屏障机制, 栈空间不启动,堆空间启动。整个过程几乎不需要STW,效率较高。

几乎不需要STW:GC标记准备阶段和GC标记结束阶段还是需要STW 来通知所有的Goroutine 我进入垃圾回收阶段了,然后启用/关闭屏障机制,但是这个耗时很小,一般几十微秒

混合写屏障的标记过程中不再需要STW,标记准备 和 标记结束 还是需要STW的

posted @ 2021-11-10 23:39  cheems~  阅读(385)  评论(0编辑  收藏  举报