本文介绍垃圾回收算法。以 Golang 为例。

Golang 有三种触发 GC 的方式:

  • 系统定时触发。若两分钟内没有触发 GC,则会自动触发一次。

  • 系统显式触发。用户调用 runtime.GC 方法强制触发 GC。

  • 申请内存时触发。申请堆空间时可能触发 GC。

垃圾回收算法分为两种:

  • 引用计数。

  • 标记清除算法。

标记清除算法

标记清除算法分为两个阶段:

  • 标记阶段:从 GCRoot 对象开始,遍历所有直接引用和间接引用的对象,并将对象标记为“被引用”。

  • 清除阶段:遍历中的所有对象,将所有未标记为“被引用”的对象删除。

删除对象并不是意味着回收内存,只是被删除对象的内存标记为可分配内存。

GCRoot 对象有多种来源,通常包括了:

  • 栈上的局部变量。

  • 全局变量和静态变量(位于 .bss/.data 段)

  • 寄存器中的引用。

  • 线程的栈帧。

Golang 在编译代码时会生成一些元信息,这些元信息可供 GC 是用以准确发现指针的位置。这些元信息包括了:

  • 局部变量表:函数运行时会创建一个栈帧,这个栈帧中包括了方法的参数和局部变量。

清除阶段相对比较简单,因为内存分配器中记录了内存分配的详情。

标记清除算法逻辑比较简单,但是内存碎片比较严重。

三色标记算法

标记清除算法将对象标记为两种颜色,这导致 GC 过程中会导致 STW(Stop The World)。这是若对象和 GC 并发执行,则新创建的对象在第一个阶段未被标记为可访问,从而在第二个阶段导致内存被错误地回收,因此 GC 过程中需要 STW 以避免新的内存分配。要解决这个问题,就需要是用三色标记算法。

三色标记算法将对象分为黑白灰三种对象,GC 过程如下:

  1. 所有对象初始时都是白色。

  2. GC 第一次扫描将 GCRoots 全部标记为灰色。

  3. 将灰色集合中所有没有子引用的对象标记为黑色,并将所有子引用对象标记为灰色。

  4. 重复上述步骤,直至所有对象都被标记为黑色。

  5. 遍历堆对象,删除所有的白色对象。

标记过程和用户代码是并发执行的,这就可能出现两种情况:

  • 被标记为黑色对象的引用链被断开转变为白色,导致此次 GC 没有删除。这种情况下此对象不进行额外处理,对象的删除被推迟到下一次。

  • 与灰色对象相连的白色对象断开了引用,又与黑色对象建立了引用,从而导致不可能被标记为灰色,对象被错误地理解为白色。

针对第二种情况有两种解决办法:

  1. 若黑色对象被插入了新的引用,则黑色对象会被记录下来,标记结束后再扫描一次。

  2. 若灰色对象解除了引用,则将被解除的白色引用记录下来,标记结束后以白色引用为根再进行一次扫描。

分代回收算法

分代回收算法是是内存管理方式的一种,和上面的标记清除算法并不冲突。分代回收算法将对象根据生命周期进行划分,从而减少内存碎片。分代回收将内存分为下面区域:

Diagram

内存管理流程如下:

  1. 新创建的对象被分配到 Eden 中。

  2. Eden 满,触发一次垃圾回收。将 Eden 中的对象对象移到幸存区 To 中。然后交换 From 和 To。

  3. 对幸存区 To 进行一次垃圾回收。

  4. 每次垃圾回收都会导致对象的寿命加一,当寿命超过阈值后就会将对象移到老年代中。

  5. 若老年代也满了,则触发一次 Full GC。

Last moify: 2022-12-04 15:11:33
Build time:2025-07-18 09:41:42
Powered By asphinx