GoLang之GC系列二(增量式垃圾回收)

GoLang之GC底层(增量式垃圾回收)

1.增量式垃圾回收

我们上一次的介绍都是在暂停用户程序,只专注于进行垃圾回收的前提下展开的,也就是所谓的STW(Stop The World)。
这首当其冲的问题就是:
用户程序可以接受长时间的暂停吗?
实际上,我们总是希望能够尽量缩短STW的时间,所以又出现了“增量式垃圾回收”。

增量式垃圾回收是指用户程序与垃圾回收交替执行,将垃圾回收工作分多次完成,也将暂停的时间分摊到多次,进而缩短每次暂停的时间。

但是这也带来了额外的问题,交替执行的过程中,保不齐垃圾回收程序前脚刚把一个变量标记为垃圾,用户程序后脚又用到了它。 若是放任不管,垃圾回收程序就会把有用数据“误判”为垃圾,进而影响程序正常执行。

image-20220310110429726

image-20220310100436357

2.三色抽象

在介绍标记—清扫算法时,我们使用“三色抽象”来描述回收过程中数据颜色的变化。实际上,“三色抽象”适用于描述多种垃圾回收算法的推进过程:

(1)最初所有数据都可描述为白色对象,代表尚未处理;

(2)灰色对象代表尚未处理完;

(3)黑色对象表示处理结束且不为垃圾。
所以不要把给对象着色狭隘的理解为标记——清扫类算法中修改数据颜色标记的操作,着色操作可以是广义的。
对应到复制式回收器的推进过程:最初,所有From空间的对象都为白色;回收开始后,将可达对象复制到To空间,就是复制式回收器把对象着为黑色所对应的操作。而与黑色对象对应的From空间的陈旧副本仍为白色,它们与其它垃圾对象都会被回收。

强调“三色抽象”的普适性是有意义的,因为基于“三色抽象”可以很方便的描述垃圾回收器会错误地回收可达对象的情况,进而指导读写屏障技术规避这些情况。

3.垃圾错误回收

这里我们要再次用到三色抽象了,因为它不仅可以简介的展示追踪式垃圾回收的推进过程,还可以帮助推演回收器的正确性

image-20220310101140355

回收器会错误的回收可达对象必须要同时满足如下条件:
第一:黑色对象中存在对白色对象的引用;
第二:不能从任何灰色对象追踪到该白色对象。

在这里插入图片描述

即在三色抽象中,黑色对象已经处理完毕,不会被再次扫描,而灰色对象还会被回收器继续处理,所以若出现黑色对象到白色对象的引用,同时没有任何灰色对象可以抵达这个白色对象,它就会被判为垃圾(白色)

image-20220310101728946

image-20220310111245261

4.强三色不变式

若直接不允许存在黑色对象到白色对象的引用,那就更安全了,这被称为强三色不变式

即但实际上它应该存活数据,“三色抽象”清晰的描述了垃圾回收中把存活对象误判为垃圾的情况,如果能够做到不出现黑色对象到白色对象的引用就必然不会出现这样的错误了,这被称为“强三色不变式“;

image-20220310102426787

5.弱三色不变式

我们知道,黑色对象是不会被回收器重新处理的,而会被回收器处理的灰色对象又不能抵达这个白色对象,那么它就会被作为垃圾回收,但实际上它是可达的。
如果能保障被黑色对象引用的白色对象处在灰色对象的可达路径内,就能保护它不被错误地回收了,这被称为弱三色不变

即若把条件放宽一点,允许出现黑色对象到白色对象的引用,但是可以保证通过灰色对象可以抵达该白色对象就也避免错误的回收,这被称为“弱三色不变式”

image-20220310102411923

实现强弱三色不变式的通常做法是建立“读/写”屏障

6.写屏障

若用户程序对回收相关节点进行了写操作,通常的办法就是建立写屏障。

写屏障会在写操作中插入指令,目的是把数据对象的修改通知到垃圾回收器。所以写屏障通常都要有一个记录集。

image-20220310105134864

(1)如果记录精确到被修改的每个数据对象,无疑会加大写屏障的执行开销,但相应的垃圾回收器拿到记录集后,重新扫描处理的工作量就会减少;
(2)反之,如果写屏障记录的粒度很粗,例如只精确到内存页,也就是只记录哪些内存页有数据需要重新处理,那么写屏障的执行开销就会降低,而垃圾回收重新扫描处理的工作量就会加大。

所以实现写屏障不得不考虑记录集的实现方式,包括记录集的结构、记录的精度等等,然后再结合具体的垃圾回收器类型,设计写屏障的具体实现方案,当然记录集是采用顺序存储、还是使用哈希表,记录精确到被修改的对象还是只记录其所在页等问题,就是写屏障的具体实现要考虑的了

例如,在标记——清扫类的垃圾回收器中,写屏障涉及到对数据标记颜色的修改,目的是在数据对象被修改后垃圾回收器可以重新扫描并正确标记相关节点。

再例如,在分代垃圾回收器中,除了各分代中要根据各自采用的垃圾回收器类型来设置写屏障外,还要在分代间增加写屏障,因为新生代执行垃圾回收的频率高于老年代,所以分代间写屏障要记录老年代到新生代的引用。这样在执行新生代GC时,就可以一并处理被记录下来的老年代对象了。

这是写操作的问题,接下来再看看读操作的问题。

7.读屏障

读操作在非移动式垃圾回收器中无影响,但是在复制式回收器或者压缩回收器中,由于会移动数据来避免碎片化,所以垃圾回收器和用户程序交替执行时,读数据便也不那么安全了。

例如复制式回收器已经把数据对象复制到To空间去了,之后交替执行的用户程序却读取了From空间中的陈旧对象。而垃圾回收器自认为对b的操作已经结束了,所以当From空间整体被回收,对原From空间中陈旧对象的访问便会出错。

这种情况下,就需要建立读屏障来确保用户程序不会访问到已经存在副本的陈旧对象。例如,检测到引用对象已经存在新副本,就读取To空间的新副本,或者是将新加载引用的目标对象复制到To空间等等。

例如在复制式回收中,已经被复制到To空间的a’属于黑色对象,而仍在From空间的对象b尚未被回收器扫描到,属于白色对象。

若接下来用户程序执行时,增加了对象a’对b的引用,那么对From空间对象的读取操作触发读屏障,直接把b复制到To空间,相当于把b着为黑色,避免出现黑色对象到白色对象的引用,这里的读屏障便遵循了强三色不变式,确保了回收正确性。

image-20220310112347087

8.插入写屏障

"强三色不变式"提醒我们关注白色指针向黑色对象的写入操作,无论如何,都不允许出现黑色对象到白色对象的引用,可以把白色指针着为灰色,也可以把写入的黑色对象退回到灰色,这些都属于插入写屏障

image-20220310104338303

再来看一个例子,在标记——清扫回收中,A已经被标记为黑色,若增加A到白色对象C的引用,可以通过写屏障直接把C标记为灰色(如下图(a));也可以把A回退到灰色状态(如下图(b))。无论哪一种都可以保障C被回收器发现。

这个示例是给黑色对象增加引用时触发写屏障,所以也被称为插入写屏障

图a:

图b:
图片

9.删除写屏障

"弱三色不变式"则提醒我们关注对那些到白色对象路径的破坏行为,例如要删除灰色对象到白色对象的引用时,可以把白色对象着为灰色,这种写屏障属于“删除”写屏障

image-20220310105233675

下面再来看一个删除引用的例子
(1)对象A为黑色,B为灰色,C为白色,B中有指向C的引用;
(2)接下来增加A到C的引用,此时通过灰色对象B仍可追踪到C;
(3)在回收器发现C之前删除B到C的引用。

这里出现了黑色对象A到白色对象C的引用,并且没有灰色可达路径保护,所以C将被错误的回收。

图片

如果使用插入写屏障,会在增加A到C的引用时把C着为灰色,或者把A回退到灰色。

image-20220310113459302

不过还有一种不同的策略,那就是在删除灰色对象B到白色对象C的引用时,把C着为灰色,这一操作会在删除引用时触发,所以被称为删除写屏障。虽然这样的操作有可能造成浮动垃圾,但是同样可以保障正确性。

图片

至此我们的讨论都还还在单核的讨论中,而实际的应用中 不得不讨论多核的场景

10.并行垃圾回收

并行垃圾回收,指的是暂停用户程序,多线程并行执行垃圾回收程序的场景。如下图所示:

如果回收器要并行执行,那么原本由一个线程全权负责的任务,现在需要分几份交给多个线程,那么分工不均就会导致有的线程忙死,有的线程闲死。

直接将内存划分不同区域交给各个线程的方式虽简单,却往往不能保障满意的负载均衡。而实现线程间工作转移,可以实现较好的负载均衡,却会增加线程间的同步开销。

并行场景下,“同步”似乎是不可回避的问题,否则,就连一轮垃圾回收何时结束也很难愉快的决定。但是不同的同步方式会带来不同的时间和空间开销,所以值得仔细斟酌。

除了一些共性的问题之外,不同类型的垃圾回收器在并行场景下,可能还得处理一些个性化的问题。
例如会移动数据的复制式回收器在并行场景下必须要避免数据对象被不同线程重复复制,否则可能造成数据不一致。
虽然像标记——清扫类垃圾回收器,重复处理同一个数据对象不会对回收正确性造成什么影响,但是考虑到性能,也是应该尽量避免的。

image-20220310110141233

12.并发垃圾回收

并发垃圾回收指的是用户程序与垃圾回收程序并发执行,在单核场景下,就等价于增量式垃圾回收,因为二者只能交替执行,如下图(a)所示;而在多核场景下,就会存在用户程序和垃圾回收程序并行执行的情况了,如下图(b)所示。这和并行垃圾回收中,只考虑垃圾回收程序的并行执行是不同的。

image-20220310110159893

13.并发与写屏障

并发场景下,依然需要使用读写屏障保障程序正确执行。不同的是,单核场景下,用户程序与垃圾回收程序不会同时执行,所以用户程序执行写屏障进行记录时,垃圾回收程序不会使用写屏障的记录集。
但在多核并发场景下就不一定了,所以并发写屏障的设计还要考虑到用户程序之间,以及与垃圾回收程序之间的竞争问题。

14.主体并发回收

如果没有任何STW的时间,也就是说垃圾回收程序与用户程序完全并发执行,其代价与实现难度可能都会高于短暂的STW。

例如标记——清扫回收器中,若完全抛弃STW,那么垃圾回收开始的消息便很难准确及时地通知到所有线程,可能导致某些线程开启写屏障的动作有所延迟而无法保障双方执行的正确性。

所以实际应用中,在某些阶段适当采取STW的方式,在其它阶段支持并发的主体并发回收更容易实现,如下图(a)所示。若在此基础上再支持增量式回收,就如下图(b)所示,那便属于主体并发增量式回收

在这里插入图片描述

垃圾回收器的设计要在保障正确性的基本前提下,力求实现更短的暂停时间、更大的吞吐量、更小的空间开销等目标。所以是否要支持并发,支持到何种程度,也都需要根据特定场景做出权衡。

就像有些分代回收器会在新生代使用STW的方式,而在老年代支持并发,便是结合新生代垃圾回收的特点,以及同步与写屏障的实现代价等多方因素而设计的方案。

不得不说,垃圾回收是一门艺术,而我们只对它进行了相当粗线条的描绘。

15.总结

Golang中GC的实现采用的是标记——清扫算法,支持并发与增量式回收。那Golang中的GC是如何应对诸如内存碎片化、并发写屏障、同步等经典问题的呢?请看下节

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

GoGo在努力

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值