go的垃圾回收设计原理

收集器的行为:

收集器有三个工作阶段,

  • Mark Setup - STW
  • Marking - Concurrent
  • Mark Termination - STW

Mark Setup - STW

必须执行的第一个活动是打开写入屏障。写入屏障的目的是允许收集器在收集期间保持堆上的数据完整性,因为收集器和应用程序 goroutine 将同时运行。

为了打开写入屏障,必须停止运行的每个应用程序 goroutine。这种活动通常非常快,平均在 10 到 30 微秒内。也就是说,可以保证应用程序 goroutine 正常运行

内存管理的组件

用户程序(Mutator)会通过内存分配器(Allocator)在堆上申请内存,而垃圾收集器(Collector)负责回收堆上的内存空间,内存分配器和垃圾收集器共同管理着程序钟的堆内存空间。

标记清除

标记清除(Mark-Sweep)算法是最常见的垃圾收集算法,标记清除收集器是跟踪式垃圾收集器,其执行过程可以分成标记(Mark)和清除(Sweep)两个阶段:

1.标记阶段-从根对象出发查找并标记堆中所有存活的对象;

2.清除阶段-遍历堆中的全部对象,回收未被标记的垃圾对象并将回收的内存加入到空闲链表;

标记阶段

内存空间中存在多个对象,从根节点可达的对象都会被标记为存活状态,而不可到达的对象会被当做垃圾

清除阶段

标记阶段结束后进入清除阶段,在阶段中收集器会依次遍历堆中的所有对象,释放其中没有被标记的对象,并将新的空闲空间以链表的机构串联起来,方便内存分配器使用

传统的标记清除法,垃圾收集器从垃圾收集的根对象出发,递归遍历这些对象指向的子对象并将所有可达到的对象标记成存活;标记阶段结束后,垃圾收集器会依次遍历堆中的对象并清除其中的垃圾,整个过程需要标记对象的存活状态,用户程序在垃圾收集的过程中也不能执行,需要用到更复杂的机制来解决STW的问题

三色抽象

为了解决原始标记清除算法带来的长时间STW,多数现代的追踪式垃圾收集器都会实现三色标记算法的变种以缩短STW的时间。三色标记算法将程序中的对象分为白色,黑色,灰色三类:

白色对象-潜在的垃圾,其内存可能被垃圾收集器回收;

黑色对象-活跃的对象,包括不存在任何引用外部指针的对象以及从根对象可达的对象;

灰色对象-活跃的对象,因为存在指向白色对象的外部指针,垃圾收集器会扫描这些对象的子对象

垃圾收集器的工作方式:

在垃圾收集器开始工作的时候,程序中不存在黑色对象,垃圾收集器的根节点被标记为灰色,垃圾收集器只会从灰色的节点开始遍历,被遍历到节点会被标记为黑色,当所有可以从节点遍历到的节点都被标记为黑色的时候,标记阶段就会结束

三色标记垃圾收集器的工作原理归纳:

1.从灰色对象的集合中选择一个灰色对象并标记成黑色;

2.将黑色对象指向的所有对象都标记成灰色,保证该对象和被对象引用的对象都不会被回收;

3.重复上述两个步骤知道对象图中不存在灰色对象;

当三色的标记清除的标记阶段结束之后,应用程序的堆中就不存在任何的灰色对象,我们只能看到黑色的存活对象以及白色的垃圾对象,垃圾收集器可以回收这些白色的垃圾

因为用户程序可能存在标记执行的过程中修改对象的指针,所以三色标记清除算法,本身是不可以并发或者增量执行的,仍然需要STW ,在如下所示的三色标记过程中,用户程序建立了从A对象到D对象的引用,但是程序中已经找不到灰色的根节点对象,所以D对象会被垃圾收集器错误地回收

本来不应该被回收的对象却被回收了,这在内存管理中是非常严重的错误,我们将这种错误称为悬挂指针,即指针没有指向特定类型的合法对象,影响了内存的安全性,想要并发或者增量地标记对象还是需要使用屏障技术

屏障技术

内存屏障技术是一种屏障指令,它可以让CPU或者编译器在执行内存相关操作时遵循特定的约束,目前多数的现代处理器都会乱序执行指令以最大化性能,但是该技术能保证内存操作的顺序性,在内存屏障前执行的操作一定会先于内存屏障后执行的操作。

要想在并发或者增量标记算法中保证正确性,我们需要达到以下两种三色不变性中的一种

强三色不变性-黑色对象不会指向白色对象,只会指向灰色对象或者黑色对象

弱三色不变性-黑色对象指向的白色对象必须包含一条从灰色对象经由多个白色对象的可达路径

上图分别展示了遵循强三色不变性和弱三色不变性的堆内存,遵循上述两个不变性中的任意一个,我们都能保证垃圾收集算法的正确性,而屏障技术就是在并发或者增量标记过程中保证三色不变性的重要技术

垃圾收集中的屏障技术更像是一个钩子方法,它是在用户程序读对象,创建新对象以及更新对象指针时执行的一段代码,根据操作类型的不同,屏障分为读屏障和写屏障两种,因为读屏障需要在读操作中加入代码片段,对用户程序的性能影响很大,所以编程语言往往都会采用写屏障保证三色不变性。

插入写屏障

通过写屏障用户程序和垃圾收集器可以在交替工作的情况下保证程序执行的正确性

伪代码:

writePointer(slot, ptr):

shade(ptr)

*slot = ptr

代码解释:

每当执行类似 *slot = ptr的表达式时,我们会执行上述写屏障通过shade函数尝试改变指针的颜色。如果ptr指针是白色的,那么该函数会将该对象设置成灰色,其他情况则保持不变

假设我们在应用程序中使用Dijkstra提出的插入写屏障,在一个垃圾收集器和用户程序交替运行的场景中会出现如图所示的标记过程:

1.垃圾收集器将根对象指向A对象标记成黑色并将A对象指向的对象B标记成灰色;

2.用户程序修改 A 对象的指针,将原本指向 B 对象的指针指向 C 对象,这时触发写屏障将 C 对象标记成灰色

3.垃圾收集器依次遍历程序中的其他灰色对象,将它们分别标记成黑色

Dijkstra插入写屏障的缺点

Dijkstra的插入写屏障相对保守,它会将有存活可能的对象都标记成灰色以满足强三色不变性。如上图中的B对象已经不能从根节点访问到,属于垃圾,但是也被标记为黑色。还有当我们在第二步和第三步之间加上一步,就是将指向C对象的指针改回指向B,因为B已经被标记为黑色,所以仍不会被当成垃圾回收

插入式的 Dijkstra 写屏障虽然实现非常简单并且也能保证强三色不变性,但是它也有明显的缺点。因为栈上的对象在垃圾收集中也会被认为是根对象,所以为了保证内存的安全,Dijkstra 必须为栈上的对象增加写屏障或者在标记阶段完成重新对栈上的对象进行扫描,这两种方法各有各的缺点,前者会大幅度增加写入指针的额外开销,后者重新扫描栈对象时需要暂停程序,垃圾收集算法的设计者需要在这两者之间做出权衡

删除写屏障

Yuasa在1990年的论文Real-time garbage collection on general-purpose machines 中提出了删除写屏障,因为一旦该写屏障开始工作,它会保证开启写屏障时堆上所有对象的可达,所以也被称为快照垃圾收集。

伪代码:

writePointer(slot, ptr)

shade(*slot)

*slot = ptr

该写屏障保证增量或者并发执行垃圾收集时程序的正确性

举例

1.垃圾收集器将根对象指向 A 对象标记成黑色并将 A 对象指向的对象 B 标记成灰色;

2.用户程序将 A 对象原本指向 B 的指针指向 C,触发删除写屏障,但是因为 B 对象已经是灰色的,所以不做改变;

3.用户程序将 B 对象原本指向 C 的指针删除,触发删除写屏障,白色的 C 对象被涂成灰色;

4.垃圾收集器依次遍历程序中的其他灰色对象,将它们分别标记成黑色

上述的第三步触发了Yuasa删除写屏障的着色,因为用户程序删除了B指向C的指针,所以C和D两个对象分别违反强三色不变性和弱三色不变性:

  • 强三色不变性 — 黑色的 A 对象直接指向白色的 C 对象;
  • 弱三色不变性 — 垃圾收集器无法从某个灰色对象出发,经过几个连续的白色对象访问白色的 C 和 D 两个对象;

Yuasa删除写屏障通过对C对象的着色,保证了C对象和下游的D对象能够在这次垃圾收集的循环中存活,避免发生悬挂指针以保证用户程序的正确性。

增量和并发

传统的垃圾收集算法会在垃圾收集的执行期间暂停应用程序,一旦触发垃圾收集,垃圾收集器会

抢占CPU的使用权占据大量的计算资源以完成标记和清除工作,然而很多追求实时的应用程序无法接受长时间的STW

解决办法

增量垃圾收集-增量地标记和清除垃圾,降低应用程序暂停的最长时间

并发垃圾收集-利用多核的计算资源,在用户程序执行时并发标记和清除垃圾

这两种解决方式,都可以保证用户程序和垃圾收集器,交替运行,所以需要使用屏障技术保证垃圾收集的正确性;与此同时,应用程序也不能等到内存溢出时触发垃圾收集,因为当内存不足时,应用程序已经无法分配内存,这与直接暂停程序没有区别,增量和并发的垃圾收集需要提前触发并在内存不足前完成整个循环,避免程序的长时间暂停

增量收集器

增量式的垃圾收集器是减少程序最长暂停时间的一种方案,它可以将原本时间较长的暂停时间切分成更多个更小的GC时间片,虽然从垃圾收集器开始到结束的时间更长了,但是也减少了应用的程序暂停的最大时间

需要注意点:

增量式垃圾收集需要与三色标记法一起使用,为了保证垃圾收集的正确性,需要在垃圾收集开始前打开写屏障,这样用户程序修改内存都会先经过写屏障处理,从而保证了堆内存中对象关系的强三色不变性或者弱三色不变性。

缺点:

增量式垃圾收集,会增加一次GC循环的总时间,在垃圾收集期间,因为写屏障的影响用户程序也需要承担额外的计算开销

并发收集器

并发的垃圾收集不仅能够减少程序的最长暂停时间,还能减少整个垃圾收集阶段的时间,通过开始读写屏障,利用多核优势与用户程序并行执行,并发垃圾收集器确实能够减少垃圾收集堆应用程序的影响:

虽然并发收集器能够与用户程序一起运行,但是并不是所有阶段都可以与用户程序一起运行,部分阶段还是需要暂停用户程序的,不过与传统的算法相比,并发的垃圾收集可以将能够并发执行的工作尽量并发执行;当然,因为读写屏障的引入,并发的垃圾收集器也一定会带来额外开销,不仅会增加垃圾收集的总时间,还会影响用户程序,这是我们在设计垃圾收集策略时必须要注意的

  • 23
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值