Golang-GC(垃圾回收)的工作原理

在垃圾回收领域,常见算法有标记清除(Mark - Sweep)和引用计数(Reference Count),Go 语言采用的是标记清除算法,并在此基础上借助三色标记法和写屏障技术提升效率。

标记清除算法基础

标记清除收集器属于跟踪式垃圾收集器,执行过程分为标记(Mark)与清除(Sweep)两个阶段:

  • 标记阶段:从根对象出发,查找并标记堆中所有存活对象。根对象通常指全局变量、栈上的变量等。

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

该算法存在一个显著问题,即在标记期间需要暂停程序(Stop the world,STW),直到标记结束,用户程序才能继续执行。为减少 STW 时间,实现异步执行,Go 语言引入了三色标记法。

三色标记法

三色标记算法将程序中的对象分为白色、黑色和灰色三类:

  • 白色:代表尚未被垃圾回收器访问到的对象。在标记结束后,白色对象会被认定为垃圾对象,等待回收。

  • 灰色:表示存活对象,但其子对象还未被处理。

  • 黑色:意味着存活对象,且其所有子对象均已被处理。

标记开始时,所有对象都被放入白色集合(此步骤需 STW)。接着,根对象被标记为灰色,并加入灰色集合。垃圾搜集器从灰色集合中取出一个对象,将其标记为黑色,同时将该对象指向的对象标记为灰色并加入灰色集合。重复此过程,直至灰色集合为空,标记阶段结束。此时,白色对象即为需要清理的对象,而黑色对象都是根可达的存活对象,不会被清理。

  • 标记过程:
    • 从根对象开始,将根对象标记为灰色并放入灰色集合。
    • 取出灰色集合中的对象,将其标记为黑色,并将其指向的未标记对象标记为灰色放入集合。
    • 重复上述步骤,直到灰色集合为空,此时白色对象为可回收对象。

不过,三色标记法虽允许标记阶段并发执行,但由于引入了白色状态来存放不确定对象,可能会导致一些对象遗漏标记的情况。具体来说,三色标记法是一个存在假阴性(false negative)的算法,即可能把本应回收(不可达)的对象错误地判定为存活对象。在 Go 的垃圾回收机制中,虽通过写屏障等手段尽量避免这种情况,但从理论上该算法存在此特性。

三色标记法并发执行时,还面临一个问题:在 GC 过程中对象指针可能发生改变。例如,假设初始对象引用关系为

A (黑) -> B (灰) -> C (白) -> D (白)

正常情况下,D 对象最终会被标记为黑色,不应被回收。然而,在标记和用户程序并发执行过程中,若用户程序删除了 C 对 D 的引用,同时 A 获得了 D 的引用,变为

A (黑) -> B (灰) -> C (白) ,A -> D (白)

由于 A 已在之前的标记过程中被处理为黑色,在当前标记阶段不会再次遍历其引用对象,从而导致 D 无法被标记为黑色,最终可能被误判为垃圾对象回收。

写屏障技术

为解决三色标记法并发执行时对象指针改变导致的问题,Go 语言使用了内存屏障技术。内存屏障是在用户程序读取对象、创建新对象以及更新对象指针时执行的一段代码,类似一个钩子。垃圾收集器采用了写屏障(Write Barrier)技术,当对象新增或更新时,会将其着色为灰色。如此一来,即便与用户程序并发执行且对象引用发生改变,垃圾收集器也能正确处理。

一次完整的 GC 过程

一次完整的 GC 分为以下四个阶段:

  • 标记准备(Mark Setup,需 STW):此阶段打开写屏障。打开写屏障的目的是为了在后续并发标记阶段,确保对象引用关系的改变能被垃圾回收器正确追踪。

  • 使用三色标记法标记(Marking, 并发):利用三色标记法,从根对象开始标记存活对象。

  • 标记结束(Mark Termination,需 STW):关闭写屏障。标记结束后,不再需要通过写屏障来维护对象标记状态的正确性,关闭它可以避免额外的性能开销。

  • 清理(Sweeping, 并发):遍历堆,回收未被标记的白色对象,将这些内存空间加入空闲链表供后续使用。

posted @ 2023-07-21 06:48  GJH-  阅读(8)  评论(0)    收藏  举报