【Golang】 关于Go语言垃圾回收的历史进程

垃圾回收历史进程

一、为什么选择标记清除

1、判断对象存活的思路

在 GC 领域里,判断对象存活的主流思路是两个,「引用计数」和「可达性分析」。

2、引用计数

顾名思义,引用计数的思路就是给每个对象进行计数,每被其它对象引用一次,计数就 +1,引用失效后,计数就 -1。当计数器的数值为 0,就意味着它没有被使用,可以回收。

但是引用计数法有个缺陷就是不能解决循环引用的问题。循环引用是指对象 A 和对象 B 互相持有对方的引用。这样两个对象的引用计数都不是 0 ,因此永远不能被收集。另外的缺陷是,每次对象的赋值都要将引用计数加一,增加了消耗。

但是它也并不是一无是处,它的回收实时性效果更好,可以配合「可达性分析」一起使用,发挥各自的优点,在不同的场景下使用不同的策略。

3、可达性分析

可达性分析的思路就是通过引用链路判断对象是否可被触达,如果能触达说明该对象当前正在被使用,不可回收;反之,没有触达到的对象则认为是无使用的,可以回收。这个引用链路的结构类似于有向有环图,但是根节点不止一个,是一个集合,称之为 GCRoots。

目前主流的 GC 机制大多用的是「可达性分析」这条路线。Go、Java、.Net等都是如此。

由于「可达性分析」思路是主流,所以后续发展出来的很多回收算法都以这个思路为基础的,三色标记法就是其中之一。我们今天主要来聊聊它。

4、分代收集

分代收集也是传统 Mark-Sweep 的一个改进。这个算法是基于一个经验:绝大多数对象的生命周期都很短。所以按照对象的生命周期长短来进行分代。

一般 GC 都会分三代,在 java 中称之为新生代(Young Generation)、年老代(Tenured Generation)和永久代(Permanent Generation);在 .NET 中称之为第 0 代、第 1 代和第2代。

原理如下:

  • 新对象放入第 0 代
  • 当内存用量超过一个较小的阈值时,触发 0 代收集
  • 第 0 代幸存的对象(未被收集)放入第 1 代
  • 只有当内存用量超过一个较高的阈值时,才会触发 1 代收集
  • 2 代同理

因为 0 代中的对象十分少,所以每次收集时遍历都会非常快(比 1 代收集快几个数量级)。只有内存消耗过于大的时候才会触发较慢的 1 代和 2 代收集。

因此,分代收集是目前比较好的垃圾回收方式。使用的语言(平台)有 jvm、.NET 。

为什么没有用分代回收,因为golang 的编译器会通过逃逸分析将大部分新生对象存储栈上(栈直接被回收)

二、Golang v1.3前:标记清除法

GoV1.3之前的标记清除: mark and sweep

  1. 暂停程序业务逻辑STW(stop the world), 分类出可达和不可达的对象,然后做上标记。

  2. 程序找出它所有可达的对象,并做上标记。

  3. 清除未标记的对象。(对象5,6不可达,被GC清除)

  4. 停止暂停,让程序继续跑。然后循环重复这个过程,直到process程序生命周期结束。

注意:mark and sweep算法在执行的时候,需要程序暂停!即 STW(stop the world),STW的过程中,CPU不执行用户代码,全部用于垃圾回收,这个过程的影响很大,所以STW也是一些回收机制最大的难题和希望优化的点。所以在执行第三步的这段时间,程序会暂定停止任何工作,卡在那等待回收执行完毕。

mark and sweep的缺点:

  1. STW,stop the world;让程序暂停,程序出现卡顿 (重要)

  2. 标记需要扫描整个heap(堆);

  3. 清除数据会产生heap碎片。

 

三、Golang v1.3 :标记清除进行步骤提前

 

标记清除逻辑:

  • 从程序的根对象出发,对于可达的对象进行标记

  • 对未能标记的对象进行删除

STW的步骤提前了一步,因为在Sweep清除的时候,可以不需要STW停止,因为这些对象已经是不可达对象了,不会出现回收写冲突等问题。但是无论怎么优化,Go V1.3都面临这个一个重要问题,就是mark-and-sweep 算法会暂停整个程序 。

四、Golang v1.5 : 三色并发标记清除进行垃圾回收

1、为什么要把标记清除优化为三色并发标记?

因为可以降低stw的时间,提高效率

2、三色标记清除法示意图和流程描述

第一步:所有对象标记为白色

第二步:每次GC开始就从根节点开始进行遍历,把遍历到的数据放入到灰色标记表

第三步:继续遍历灰色节点,把灰色对象引用的白色对象放入灰色对象,之后将灰色对象放入黑色集合

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

第五步:回收白色标记表中的对象

3、怎么解决stw时间过长的问题?

3.1、stw是什么?

是stop the world的缩写,可以理解为触发stw后整个程序会停止运行,如果不启动stw会怎么样?在三色标记回收的过程中,一个白色对象被黑色对象引用了 且 该白色对象上游没有被任何灰色对象引用 ,但是我们不会再去遍历黑色对象表,导致该白色对象被错误的回收,造成对象丢失

结论:在没有stw的情况下导致错误的回收,导致对象丢失

3.2、怎么解决尽量让stw时间短,又保证gc可用,不会出现对象丢失的情况?

强三色不变式:不存在黑色对象引用到白色对象的指针

弱三色不变式:所有被黑色对象引用的白色对象都处于灰色保护状态

解决思路:只要保证强三色不变式或者弱三色不变式中成立一个,就可以保证不会丢失对象

解决办法:

A、插入写屏障:如果黑色的 A 对象引用 白色的B对象,则B对象标记为灰色

问题:由于栈特点是容量小,速度快,所以插入屏障在栈空间中不使用,而仅仅在堆空间中使用,所以可能出现栈上发生 白色对象 被 黑色对象引用 的可能解决办法:堆上三色标记清理完后,对栈启动stw(相比对堆进行stw,这里时间会很短),然后再做一遍三色标记清除

B、删除写屏障:被删除的对象如果自身为灰色或者白色,那么被标记为灰色,则这轮不会被回收,要到下轮才可能会被回收

3.3、总结插入写屏障和删除写屏障的短板

A、插入写屏障:结束后需要stw来重新扫描栈,标记栈上引用的白色对象是否真的存活

B、’删除写屏障:回收精度低,会延迟一轮回收

五、golang v1.8 : 三色标记清除-混合写屏障

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

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

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

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

优点:避免了re-scan,极大的减少了stw的时间

满足条件:变形的弱三色不变式

分析

第一步:相当于栈上的删除写屏障:保证了不会原来被栈引用的栈对象被删除引用后,又被其他栈上对象引用,但是由于没有删除写屏障却被回收

第二步:相当于栈上的插入写屏障:保证了栈上新加对象不会由于没有写入屏障,从而被删除引用的时候被错误回收

第三步:满足删除写屏障

第四步:满足插入写屏障

 

六、总结

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

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

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

 

参考文章

https://www.cnblogs.com/cxy2020/p/16321884.html

 

posted @ 2022-10-02 21:07  踏雪无痕SS  阅读(192)  评论(0编辑  收藏  举报