土法炼钢兴趣小组的算法知识备份

Go 语言垃圾回收 (GC) 深度解析

目录

引言

Go 语言的 GC 设计哲学与 Java 截然不同。Go 专注于低延迟 (Low Latency),牺牲了一部分吞吐量来换取极短的 STW (Stop The World) 时间。Go 1.5 引入了并发标记,Go 1.8 引入了混合写屏障,使得 STW 时间通常在微秒级别。

1. 三色标记法 (Tri-color Marking)

Go 使用并发的三色标记清除算法。

Tri-color Marking

1.1 核心概念

1.2 标记过程

  1. 初始状态: 所有对象均为白色。
  2. 根扫描: 将 GC Roots 直接引用的对象标记为灰色。
  3. 并发扫描:
    • 从灰色集合中取出一个对象,将其标记为黑色。
    • 将该对象指向的所有白色子对象标记为灰色。
    • 重复此步骤,直到灰色集合为空。
  4. 完成: 此时只剩下黑色和白色对象。白色对象即为垃圾。

1.3 并发问题:漏标

在并发标记过程中,用户线程 (Mutator) 可能会修改引用关系,导致漏标(即把活跃对象误判为垃圾)。漏标发生必须同时满足两个条件: 1. 条件 1: 黑色对象引用了一个白色对象。 2. 条件 2: 灰色对象与该白色对象之间的引用关系被破坏(删除)。

为了防止漏标,必须破坏上述两个条件之一。这就是写屏障的作用。

2. 混合写屏障 (Hybrid Write Barrier)

Go 1.8 之前使用的是 Dijkstra 写屏障或 Yuasa 写屏障,Go 1.8 引入了混合写屏障,结合了两者的优点。

2.1 插入写屏障 (Dijkstra) - 破坏条件 1

2.2 删除写屏障 (Yuasa) - 破坏条件 2

2.3 混合写屏障 (Hybrid)

Go 结合了两者,极大地减少了 STW 时间(不再需要 STW 重新扫描栈)。

混合写屏障伪代码:

// slot 是指针在内存中的地址
// ptr 是要写入的新值
writePointer(slot, ptr):
    shade(*slot) // 1. 删除写屏障:将旧值置灰 (保护旧路径)
    shade(ptr)   // 2. 插入写屏障:将新值置灰 (保护新路径)
    *slot = ptr  // 3. 更新指针

3. GC Pacing (调优与触发)

Go GC 的触发不仅仅依赖于固定阈值,而是基于反馈控制算法,称为 GC Pacing

3.1 GOGC 参数

GOGC 环境变量控制了下一次 GC 的触发堆大小。默认值为 100。 \[ \text{NextGC} = \text{HeapMarked} + \text{HeapMarked} \times \frac{\text{GOGC}}{100} \]

3.2 辅助标记 (Mark Assist)

为了防止在并发标记期间,用户线程分配内存的速度超过 GC 标记的速度,Go 引入了辅助标记。 * 机制: 分配内存过快的 Goroutine 会被”征用”一部分 CPU 时间来协助进行标记工作。 * 效果: 这是一个背压 (Backpressure) 机制,限制了高分配速率的 Goroutine。

4. Green Tea GC (Go 1.25+ 实验性特性)

在 Go 1.25 中,Go 团队引入了一个代号为 Green Tea 的新 GC 实现(可通过 GOEXPERIMENT=greenteagc 开启)。这是对标记阶段的重大重构,旨在解决现代硬件上的”微架构灾难”。

4.1 背景:微架构灾难 (Microarchitectural Disaster)

传统的 GC 标记算法(Graph Flood / Mark-Sweep)在现代 CPU 上效率极低: * 随机内存访问: 追踪指针意味着在堆内存中随机跳转。 * Cache Miss: 这种随机性导致 CPU 缓存命中率极低。 * 流水线停顿: CPU 必须等待内存数据加载,导致大量指令周期被浪费(Stall)。 * NUMA 问题: 跨核心/跨插槽的内存访问延迟进一步加剧了性能损耗。

Go 团队发现,GC 标记阶段约 35% 的时间是纯粹的内存等待(Memory Stall)。

4.2 核心机制:面向页 (Page-Oriented)

Green Tea GC 的核心思想是:Work with pages, not objects. (处理页,而不是对象)。

Green Tea GC vs Graph Flood

4.2.1 数据结构变革

4.2.2 算法流程 (The Green Tea Algorithm)

Green Tea 利用”懒惰”(Laziness)来累积工作量,从而实现批量处理。

  1. 发现 (Discovery):
    • 当扫描对象 A 时,发现它引用了对象 B(位于页 P)。
    • 设置对象 B 的 seen 位。
    • 如果页 P 不在工作队列中,将页 P 加入队列。
    • 关键点: 此时立即扫描对象 B。
  2. 处理 (Processing):
    • 从工作队列中取出一个页 P。
    • 批量扫描: 遍历页 P 的元数据,找到所有 seen=1scanned=0 的对象。
    • 线性访问: 按照内存地址顺序,依次扫描这些对象。
    • 扫描完成后,更新 scanned 位。

这种机制使得 GC 能够在同一个页上一次性处理多个对象,极大地提高了空间局部性 (Spatial Locality)

4.3 向量化加速 (Vector Acceleration)

由于 Green Tea 将工作规整化为对页的处理,这使得利用 SIMD 指令集(如 AVX-512)成为可能。

4.3.1 扫描内核 (Scanning Kernel)

Green Tea 使用 AVX-512 指令集并行处理页的元数据。以下是硬件加速的逻辑步骤:

  1. 加载元数据: 将整个页的 seenscanned 位图加载到 512 位寄存器中。
  2. 计算活跃对象: \[ \text{ActiveObjects} = \text{Seen} \ \& \ (\sim \text{Scanned}) \]
  3. 位图膨胀 (Bit Expansion):
    • 利用 GFNI (Galois Field New Instructions) 指令集中的 VGF2P8AFFINEQB 指令。
    • 将”每个对象 1 位”的位图,膨胀为”每个字 (Word) 1 位”的位图。
    • 例如:如果对象大小为 3 个字,位 1 会膨胀为 111
  4. 指针过滤:
    • 加载内存分配器的 Pointer/Scalar 位图(指示内存中哪些位置是指针)。
    • \[ \text{ActivePointers} = \text{ExpandedActiveObjects} \ \& \ \text{PointerBitmap} \]
  5. 批量提取:
    • 根据 ActivePointers 位图,直接从内存中批量加载所有指针,放入缓冲区。

4.3.2 性能收益

4.4 总结与对比

特性 传统 GC (Graph Flood) Green Tea GC
工作单元 对象 (Object) 页 (Page, 8KB)
遍历顺序 深度优先 (DFS) 广度优先 (BFS)
内存访问 随机跳跃 (Random) 线性扫描 (Linear)
硬件利用 差 (Cache Miss 高) 优 (Cache Friendly + SIMD)
CPU 开销 基准 降低 10% - 40%

Green Tea 标志着 GC 算法从纯软件算法向软硬协同优化的转变。它不再把 CPU 视为黑盒,而是针对现代处理器的缓存层级和向量指令进行了深度定制。

总结

Go GC 的核心在于低延迟。 1. 并发三色标记: 减少 STW。 2. 混合写屏障: 消除栈重扫的 STW,允许极短的停顿。 3. GC Pacing: 动态调整 GC 频率和辅助标记力度,平衡吞吐量和延迟。 4. Green Tea (新): 通过页级扫描和向量化指令,进一步压榨硬件性能。


By .