Golang垃圾回收

本学习内容总结来自B站UP主"刘丹冰AceId"的视频: https://b23.tv/8Y6xwN7

垃圾回收GC

GC(Garbage Collection), 垃圾回收, 是编程语言中提供的自动的内存管理机制, 自动释放不需要的内存对象, 让出存储器资源. GC过程中无需程序员手动执行

Golang的GC演进过程:

  • Go1.3之前, 使用标记-清除(mark and sweep)算法
  • Go1.5:三色标记法
  • Go1.8:混合写屏障机制

标记-清除算法

过程

第一步:暂停程序业务逻辑(STW:Stop The World)

第二步:标记:标记所有的可达对象和不可达对象

第三步:清除:清除所有不可达对象

第四步:恢复执行程序业务逻辑

例: 对象1/2/3/4/7可达, 标记为红色, 对象5/7不可达, 不标记, 需清除

42-GC2.png

缺点

  • STW, Stop The World, 让程序暂停, 程序会出现卡顿(重要问题)

  • 标记需要扫描整个heap

  • 清除数据会产生heap碎片

三色标记法

GC过程可以和部分业务逻辑并行, 但是也会需要一定时间的STW(不像标记清除算法那样需要一大段的STW)

将对象分为三类(三色):

  • 白色: 未经过GC扫描处理的对象(GC启动时所有对象都会置为白色, GC执行完成后, 白色对象为不可达的对象, 需删除)
  • 灰色: GC处理中的对象(若还存在灰色对象, 则说明GC还没有执行完成, 否则说明GC执行完成)
  • 黑色: GC处理完成的对象(GC执行完成后, 黑色对象为可达对象, 不能删除)

过程

第一步:每次新创建的对象,默认的颜色都是标记为“白色”

47-GC5.jpeg

第二步:每次GC回收开始, 会从根节点开始遍历所有对象,把遍历到的对象从白色集合放入“灰色”集合

48-GC6.jpeg

注意: 这里类似树的广度遍历, 但只遍历一层, 如上图所示,当前可抵达的对象是对象1和对象4,那么自然本轮遍历结束,对象1和对象4就会被标记为灰色,灰色标记表就会多出这两个对象。

第三步:遍历灰色集合,将灰色对象引用的对象从白色集合放入灰色集合,之后将此灰色对象放入黑色集合

49-GC7.jpeg

这一次遍历是只扫描灰色对象,将灰色对象的第一层遍历可抵达的对象由白色变为灰色,如:对象2、对象7. 而之前的灰色对象1和对象4则会被标记为黑色,同时由灰色标记表移动到黑色标记表中。

第四步: 重复第三步, 直到灰色中无任何对象

50-GC8.jpeg

51-GC9.jpeg

当我们全部的可达对象都遍历完后,灰色标记表将不再存在灰色对象,目前全部内存的数据只有两种颜色,黑色和白色。那么黑色对象就是我们程序逻辑可达(需要的)对象,这些数据是目前支撑程序正常业务运行的,是合法的有用数据,不可删除,白色的对象是全部不可达对象,目前程序逻辑并不依赖他们,那么白色对象就是内存中目前的垃圾数据,需要被清除。

第五步: 回收所有的白色标记表的对象. 也就是回收垃圾

52-GC10.jpeg

没有STW的三色标记法

若上述的三色标记法没有STW, 即业务程序不暂停, 那么意味着在GC过程中, 每个对象都可能出现新增或者删除操作, 那么会不会出现GC回收错误, 即把不该回收的对象也回收了呢?

答案是会的, 请看下面的一个例子:

我们把初始状态设置为已经经历了第一轮遍历,目前黑色的有对象1和对象4, 灰色的有对象2和对象7,其他的为白色对象,且对象2是通过指针p指向对象3的,如图所示。

55-三色标记问题1.jpeg

GC接下来准备扫描灰色的对象2和对象7, 这时, 如果已经标记为黑色的对象4,此时创建指针q,并且指向白色的对象3

56-三色标记问题2.jpeg

与此同时灰色的对象2将指针p移除,那么白色的对象3实则就是被挂在了已经扫描完成的黑色的对象4下,如图所示。

57-三色标记问题3.jpeg

然后GC开始正常继续执行(开始扫描灰色的对象2和对象7),将所有灰色的对象标记为黑色,那么对象2和对象7就被标记成了黑色,如图所示。

58-三色标记问题4.jpeg

最后垃圾清理的时候, 本来是对象4合法引用的对象3,却被GC给“误杀”回收掉了。

因此, 当出现下面两种情况的时候, 是会造成三色标记法"误杀"的:

  • 条件1:黑色对象引用了白色对象(白色被挂载了黑色下)
  • 条件2:灰色对象指向该白色对象的连接遭到破坏(灰色同时丢了该白色)

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

强弱三色不变式

为了防止上述这种现象的发生, 最简单的方法还是使用STW, 但是STW的过程有明显的资源浪费.

我们可以使用屏障机制, 来破坏其中的一个条件(或都破坏), 达到对象不丢失的目的.这两种方式就是强三色不变式弱三色不变式

  • 强三色不变式

主要为了破坏上述的条件1(白色被挂载了黑色下), 做法是: 强制性的不允许黑色对象应用白色对象

60-三色标记问题6.jpeg

  • 弱三色不变式

主要为了破坏上述的条件2(灰色同时丢了改白色), 做法是: 可以允许黑色对象引用白色对象, 但是该白色对象也要处于灰色保护状态(即白色对象也被其他灰色对象引用)

61-三色标记问题7.jpeg

屏障机制

上述的强弱三色不变式, 是解决"误杀"的方式或思路, 具体的实现方案就是通过屏障机制, 分为"插入屏障"和"删除屏障"

插入屏障

具体操作: 在A对象引用B对象的时候, 将B对象标记为灰色(即对象被引用时, 就标记该对象为灰色)

这样能够符合"强三色不变式"的解决思路(强制性的不允许黑色对象应用白色对象)

但是, 我们知道, 内存具体可以分为"堆"和"栈", 主要区别是"栈"容量小, 但是速度很快, 因此对象变更频率也很快, 如果在栈中也是用插入屏障, 可能会因为频繁更新而影响性能, 因此只会在"堆"上启用"插入屏障"

如图, GC正在准备遍历灰色对象2和对象7之前, 由于没有STW, 此时外界想对象4添加对象8, 向对象1添加对象9的应用

66-三色标记插入写屏障5.jpeg

由于对象9在栈中, 不触发插入屏障, 对象9置为白色, 对象8在堆中, 触发插入屏障, 置为灰色

但是在GC完成后, 对象9还是会被"误杀", 因此需要在清理前, 再对栈重新进行一次三色标记扫描, 而这一次扫描前需要进行STW暂停, 禁止栈中的对象变化, 防止出现"误杀"

68-三色标记插入写屏障7.jpeg

69-三色标记插入写屏障9.jpeg

删除屏障

具体操作:被删除的对象, 如果自身为灰色或者白色, 那么就重新标记为灰色

这样能够符合"弱三色不变式"的解决思路(白色对象也要处于灰色保护状态)

74-三色标记删除写屏障3.jpeg

75-三色标记删除写屏障4.jpeg

但是对象5之前只被对象1引用, 现在删除了这个引用关系, 那么对象5此时是没有其他对象引用的, 是应该被清除的, 但是由于删除屏障, 对象5在这一轮的GC中被保留了下来, 如果引用关系没有改变的话, 在下一轮GC中会被正常删除

可见这种删除屏障的回收并不是那么准确

混合写屏障

上述插入写屏障和删除写屏障的缺点如下:

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

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

混合写屏障规则

1、GC开始将栈上的对象全部扫描并标记为黑色(之后不再进行第二次重复扫描,无需STW),

2、GC期间,任何在栈上创建的新对象,均为黑色。

3、所有被删除的对象都标记为灰色。

4、所有被添加的对象都标记为灰色。

posted @ 2023-02-14 12:16  Alex-GCX  阅读(309)  评论(0编辑  收藏  举报