0.1、索引

https://waterflow.link/articles/1664943418972

文中提到的垃圾回收算法是基于go1.16之后的,让咱们间接进入正题吧。

1、什么时候须要垃圾回收?

Go 更喜爱在堆栈上分配内存,因而大多数内存调配最终都会在栈上。 这意味着 Go 每个 goroutine 都有一个堆栈,并且在可能的状况下,Go 会将变量调配给这个堆栈。 Go 编译器试图通过执行逃逸剖析来查看对象是否被内部变量援用。 如果编译器能够确定一个变量的生命周期,它将被调配到一个堆栈中。 然而,如果变量的生命周期不明确,它将在堆上调配。 通常,如果 Go 程序有一个指向对象的指针,则该对象存储在堆上。 看看这个示例代码:

type myStruct struct {  value int}var testStruct = myStruct{value: 0}func addTwoNumbers(a int, b int) int {  return a + b}func myFunction() {  testVar1 := 123  testVar2 := 456  testStruct.value = addTwoNumbers(testVar1, testVar2)}func someOtherFunction() {  myFunction()}

咱们假如这是一个正在运行的程序的一部分,因为如果这是整个程序,Go 编译器会通过将变量调配到堆栈中来优化它。 程序运行时:

  1. testStruct 被定义并搁置在堆上的一个可用内存块中。
  2. myFunction 在函数执行时被执行并调配一个栈。 testVar1 和 testVar2 都存储在此堆栈中。
  3. 当 addTwoNumbers 被调用时,一个新的栈帧被压入栈中,并带有两个函数参数。
  4. 当 addTwoNumbers 实现执行时,它的后果返回给 myFunction 并且 addTwoNumbers 的堆栈帧从堆栈中弹出,因为它不再须要了。
  5. 指向 testStruct 的指针被定为到蕴含它的堆上的地位,并且值字段被更新。
  6. myFunction 退出并且为其创立的堆栈被清理。 testStruct 的值会始终保留在堆上,直到产生垃圾回收。

testStruct 当初在堆上并且没有剖析,Go 运行时不晓得是否依然须要它。 为此,Go 依赖于垃圾回收器。 垃圾回收器有两个要害局部,一个 mutator 和一个回收器。 回收器执行垃圾收集逻辑并找到应该开释其内存的对象。 mutator 执行利用程序代码并将新对象调配给堆。 它还会在程序运行时更新堆上的现有对象,其中包含在不再须要某些对象时使其无法访问。

2、垃圾回收器的实现

Go 的垃圾收集器是一个非分代并发三色标记革除垃圾回收器。 让咱们合成一下这些术语。

什么是分代:

因为“复制”算法对于存活工夫长,大容量的贮存对象须要消耗更多的挪动工夫,和存在贮存对象的存活工夫的差别。须要程序将所领有的内存空间分成若干分区,并标记为年老代空间和年轻代空间。程序运行所需的存储对象会先寄存在年老代分区,年老代分区会较为频密进行较为激进垃圾回收行为,每次回收实现幸存的存储对象内的寿命计数器加一。当年老代分区存储对象的寿命计数器达到肯定阈值或存储对象的占用空间超过肯定阈值时,则被挪动到年轻代空间,年轻代空间会较少运行垃圾回收行为。个别状况下,还有永恒代的空间,用于波及程序整个运行生命周期的对象存储,例如运行代码、数据常量等,该空间通常不进行垃圾回收的操作。 通过分代,存活在局限域,小容量,寿命短的存储对象会被疾速回收;存活在全局域,大容量,寿命长的存储对象就较少被回收行为解决烦扰。——维基百科

分代垃圾回收器专一于最近调配的对象。 然而,如前所述,编译器优化容许 Go 编译器将具备已知生命周期的对象调配给堆栈。 这意味着更少的对象将在堆上,因而更少的对象将被垃圾回收。 这意味着在 Go 中不须要分代垃圾回收器。 因而,Go 应用了非分代垃圾回收器。 并发意味着回收器与 mutator 线程同时运行。 因而,Go 应用非分代并发垃圾回收器。 标记和革除是垃圾回收器的类型,三色是用于实现它的算法。

Go 通过几个步骤实现了这一点:

1、开启写屏障

Go 通过一个名为 stop the world 的过程让所有 goroutine 达到垃圾回收平安点。 这会临时进行程序运行并关上写屏障以保护堆上的数据完整性。 这通过容许 goroutine 和回收器同时运行来实现并发。

想要在并发或者增量的标记算法中保障正确性,咱们须要达成以下两种三色不变性(Tri-color invariant)中的一种:

  • 强三色不变性 — 彩色对象不会指向红色对象,只会指向灰色对象或者彩色对象;
  • 弱三色不变性 — 彩色对象指向的红色对象必须蕴含一条从灰色对象经由多个红色对象的可达门路;

一旦所有的 goroutine 都关上了写屏障,Go 运行时就会starts the world并让workers执行垃圾回收工作。

2、标记阶段

标记是通过应用三色算法实现的。 当标记开始时,根对象是灰色的,其余对象都是红色的。 根是所有其余堆对象的源对象,并作为运行程序的一部分被实例化。 垃圾回收器通过扫描堆栈、全局变量和堆指针开始标记以理解正在应用的内容。 扫描堆栈时,workers 进行 goroutine 并通过从根向下遍历将所有找到的对象标记为灰色。 扫描实现复原 goroutine。

三色标记的工作原理:

  1. 从灰色对象的汇合中抉择一个灰色对象并将其标记成彩色;
  2. 将彩色对象指向的所有对象都标记成灰色,保障该对象和被该对象援用的对象都不会被回收;
  3. 反复上述两个步骤直到对象图中不存在灰色对象;

下图是筹备标记:

下图为当有新对象生成时,因为开启了写屏障,会间接标记为彩色

下图为根对象可达的对象都标记为彩色

3、清理阶段

而后将灰色对象排入队列以变为彩色,这表明它们仍在应用中。 一旦所有灰色物体都变成彩色,回收器将再次stop the world并清理所有不再须要的红色节点。 而后应用程序当初能够持续运行,直到它须要再次清理更多内存。

下图为STW而后清理红色对象

下图为清理之后,复原程序运行

一旦程序调配了与正在应用的内存成比例的额定内存,此过程将再次启动。 GOGC 环境变量决定了这一点,默认设置为 100。 Go 源代码将其形容为:

如果 GOGC=100 并且咱们应用 4M,咱们将在达到 8M 时再次进行 GC(此标记在 next_gc 变量中跟踪)。 这使 GC 老本与调配老本成线性比例。 调整 GOGC 只会扭转线性常数(以及应用的额定内存量)。