『每周译Go』golang 垃圾回收器如何标记内存?

共 2677字,需浏览 6分钟

 ·

2021-07-04 12:23



本文基于 Go 1.13。这里讨论的关于内存管理的概念在我的文章Go:内存管理和分配 中有解释

Go 垃圾回收器负责回收不再使用的内存。实现的算法是一个并行的三色标记扫描采集器。在本文中,我们将详细了解标记阶段,以及不同颜色的用法。

您可以在 kenfox 的可视化垃圾回收算法 中找到关于不同类型垃圾回收器的更多信息。

标记阶段

此阶段执行内存扫描,以了解代码仍在使用哪些块,以及应该回收哪些块。

但是,由于垃圾回收器可以与我们的 Go 程序同时运行,因此它需要一种在扫描时检测内存中潜在变化的方法。为了解决这个潜在的问题,实现了一个写屏障算法,允许 Go 跟踪任何指针的变化。启用写屏障的唯一条件是短时间停止程序,也称为 “STW”:



在进程开始时,Go 还会为每个处理器启动一个标记辅助进程,以帮助标记内存。

然后,一旦根节点被排队等待处理,标记阶段就可以开始遍历内存并为其着色。

现在让我们以一个简单的程序为例,该程序允许我们遵循标记阶段所做的步骤

Type struct1 struct {
    a, b int64
    c, d float64
    e *struct2
}

type struct2 struct {
    f, g int64
    h, i float64
}

func main() {
    s1 := allocStruct1()
    s2 := allocStruct2()

    func () {
        _ = allocStruct2()
    }()

    runtime.GC()

    fmt.Printf("s1 = %X, s2 = %X\n", &s1, &s2)
}

//go:noinline
func allocStruct1() *struct1 {
    return &struct1{
        e: allocStruct2(),
    }
}

//go:noinline
func allocStruct2() *struct2 {
    return &struct2{}
}

由于 struct subStruct 不包含任何指针,因此它存储在一个专用于对象的范围中,而不引用其他对象:


这使得垃圾回收器的工作更容易,因为它在标记内存时不必扫描这个范围。

一旦分配完成,我们的程序就会强制垃圾回收器运行一个周期。以下是工作流程:


垃圾回收器从堆栈开始标记,然后跟着指针递归遍历内存。直到对象都被标记时停止扫描。然而,这个过程不是在同一个 goroutine 中完成的 完成的;每个指针都在工作池中排队。然后 ,后台的标记线程发现之前的出列队列是来自该工作池,扫描对象,然后将在其中找到的指针加入队列:


着色!

后台线程现在需要一种方法来跟踪哪些内存有没有被扫描。垃圾回收器使用三色算法,其工作原理如下:

  • 所有对象一开始都被认为是白色的

  • 根对象(堆栈、堆、全局变量)将以灰色显示

完成此主要步骤后,垃圾回收器将:

  • 选择一个灰色的对象,把它涂成黑色

  • 遵循此对象的所有指针并将所有引用的对象涂成灰色

然后,它将重复这两个步骤,直到没有更多的对象要着色。从这一点来看,对象不是黑色就是白色。白色集合表示未被任何其他对象引用且准备好回收的对象。

下面是使用上一个示例对其进行的表示:



作为第一种状态,所有对象都被视为白色。然后,对象被遍历,可到达的对象将变为灰色。如果对象位于标记为 “无扫描” 的范围内,则可以将其绘制为黑色,因为不需要对其进行扫描:



灰色对象现在入队等待扫描并变黑:



在没有更多的对象要处理之前,入队的对象也会发生同样的情况:



在进程结束时,黑色对象是内存中正在使用的对象,而白色对象是要回收的对象。如我们所见,由于 struct2 的实例是在匿名函数中创建的,并且无法从堆栈访问,因此它保持为白色,可以清除。

由于每个跨度中有一个名为 gcmarkBits 的位图属性,颜色在内部实现,该属性跟踪扫描,并将相应的位设置为 1:



正如我们所见,黑色和灰色的工作原理是一样的。这一过程的不同之处在于,当黑色对象结束扫描链时,灰色对象排队等待扫描。

垃圾回收器最终会 stops the world,将每个写屏障上所做的更改刷新到工作池,并执行剩余的标记。

您可以在我的文章Go:垃圾回收器如何监视您的应用程序 中找到有关并发进程和垃圾回收器中标记阶段的更多详细信息

运行时分析器

Go 提供的工具允许我们可视化所有这些步骤,并在程序中查看垃圾回收器的影响。在启用跟踪的情况下运行我们的代码提供了前面步骤的一个大图。以下是 traces:



标记线程的生命周期也可以在 goroutine 级别的 tracer 中可视化。下面是 goroutine#33 的示例,它在开始标记内存之前先在后台等待。



浏览 33
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报