菜单

垃圾回收

相关源文件

本文档描述了 Go 的垃圾回收器(GC),它会自动回收不再使用的对象所占用的内存。Go GC 是一个并发的、三色标记清除收集器,旨在在生产环境中提供低延迟和可预测的暂停时间。

有关内存管理和分配的详细信息,请参阅内存管理

概述

Go 垃圾回收器具有以下特点:

  1. 并发 - 与应用程序(mutator)线程同时运行
  2. 类型精确 (precise) - 确切地知道哪些内存位置包含指针
  3. 非分代 - 不按年龄区分对象
  4. 非压缩 - 不移动内存中的对象
  5. 标记清除 - 使用标记阶段识别活动对象,然后进行清除阶段回收未使用的内存
  6. 基于写屏障 - 在并发标记期间跟踪指针更新

GC 周期分为几个不同的阶段,这些阶段协同工作,在最大限度地减少对正在运行的程序干扰的同时回收未使用的内存。

来源:src/runtime/mgc.go5-119 src/runtime/proc.go211-212

GC 算法和阶段

Go 垃圾回收器遵循三色标记抽象,并分阶段运行。

1. 清除终止

  • 停止世界 (STW) 暂停
  • 确保所有 P(处理器)都到达 GC 安全点
  • 完成上一 GC 周期中任何未清除的 span 的清除工作
  • 此阶段通常很短或完全跳过,除非 GC 被强制提前执行

2. 标记阶段

  • gcphase 设置为 _GCmark
  • 启用写屏障,以确保指针更新得到正确跟踪
  • 启用 mutator 辅助(分配时应用程序协助标记)
  • 准备根标记任务(堆栈、全局变量、运行时数据)
  • 启动世界 - 应用程序代码继续执行
  • 扫描与应用程序执行并发进行
    • 扫描 goroutine 堆栈以查找指针
    • 扫描全局变量
    • 扫描运行时数据结构中的堆指针
    • “灰色”对象(已发现但尚未处理)被添加到工作队列中
    • 标记工作程序通过扫描对象并标记其引用对象来消耗这些队列
  • 使用分布式终止算法来检测标记何时完成

3. 标记终止

  • 停止世界 (STW) 暂停
  • gcphase 设置为 _GCmarktermination
  • 禁用工作程序和标记辅助
  • 执行善后工作,例如刷新 mcache
  • 为清除阶段做准备

4. 清除阶段

  • gcphase 设置为 _GCoff 并禁用写屏障
  • 启动世界 - 应用程序恢复执行
  • 清除与程序执行并发进行
    • 后台清除程序 goroutine 回收内存
    • 按需清除发生在分配过程中
    • 单独清除 span 以回收空闲对象

来源:src/runtime/mgc.go24-82 src/runtime/mgc.go210-243 src/runtime/mgcscavenge.go6-18

三色标记算法

垃圾回收器使用三色标记抽象

算法如下:

  1. 最初,所有对象都为白色
  2. 根对象(堆栈、全局变量)被标记为灰色
  3. 灰色对象被扫描,其指针引用对象被标记为灰色,然后它们变为黑色
  4. 过程持续进行,直到没有灰色对象为止
  5. 剩余的白色对象是不可达的,将在清除期间被回收

写屏障通过将黑色对象引用的白色对象标记为灰色来保护“没有黑色对象指向白色对象”的不变量。

来源:src/runtime/mgcmark.go16-60 src/runtime/mbitmap.go5-54

写屏障

写屏障对于并发收集器正确工作至关重要。它确保 GC 不会错过在并发标记期间变得可达的任何对象。

写屏障:

  • 仅在标记阶段激活
  • 拦截对堆对象的指针写入
  • 确保 GC 同时看到旧指针值和新指针值
  • 防止在标记期间引用发生变化时错过对象
  • 维护“无黑色对象指向白色对象”的强三色不变性

写屏障实现在 src/runtime/writebarrier.go 中,并由 writeBarrier.enabled 标志控制,该标志在标记阶段(_GCmark_GCmarktermination)设置为 true

来源:src/runtime/mgc.go215-232 src/runtime/runtime2.go102-108

GC 调度和触发

Go 垃圾回收器使用调度算法来决定何时启动 GC 周期,平衡内存使用与 CPU 开销。

触发器

GC 可由多种条件触发:

  1. 堆增长 - 当堆大小超过基于 GOGC 设置的目标阈值时
  2. 基于时间 - 自上次收集以来已过特定时间
  3. 手动 - 调用 runtime.GC()

GOGC 设置

环境变量 GOGC 控制 GC 目标

  • 默认值为 100 (%)
  • 如果 GOGC=100 且堆使用 4MB,则 GC 将在堆达到 8MB 时触发
  • 较高的值会提高吞吐量,但可能会使用更多内存
  • 较低的值会减少内存使用,但可能会消耗更多 CPU
  • GOGC=off 禁用自动垃圾回收

GC 调度

GC 调度算法:

  • 目标是在堆达到目标大小时刚好完成下一个 GC 周期
  • 使用控制器来调整分配给 GC 的 CPU 比例
  • 平衡分配率和标记速度
  • 尽量保持一致的 GC 周期时长,无论堆大小如何

来源:src/runtime/mgc.go112-118 src/runtime/mgcpacer.go15-57

并发标记实现

Go GC 通过几种机制实现低延迟的并发标记

后台标记工作程序

  • 执行标记工作的专用 goroutine
  • gcBackgroundUtilization 控制(默认为 GOMAXPROCS 的 25%)
  • 与应用程序代码并发运行

标记辅助

  • 应用程序 goroutine 在分配内存时协助标记
  • 辅助工作量与分配大小成正比
  • 有助于平衡分配率和标记率
  • 确保 GC 即使在繁重的分配负载下也能按时完成

工作缓冲区和工作窃取

  • 标记工作组织在工作缓冲区中
  • 每个 P(处理器)都有本地工作缓冲区
  • 当 P 的本地工作缓冲区为空时,它可以从其他 P 窃取工作
  • 这种方法可以有效地在处理器之间分配工作

来源:src/runtime/mgc.go213-243 src/runtime/mgcpacer.go15-57 src/runtime/mgcmark.go16-128

清除实现

标记完成后,GC 在清除阶段回收内存

Go 中的清除分两个方面进行:

对象回收器

  • 查找并释放 span 中未标记的槽
  • 可以由 mcentral.cacheSpan 同步驱动
  • 也可以由 sweepone 异步驱动

Span 回收器

  • 查找不包含任何已标记对象的 span
  • 将整个 span 释放回堆
  • 对于新 span 的有效分配至关重要
  • 由页面标记位图的顺序扫描驱动

这两种算法最终都调用 mspan.sweep,该函数清除单个堆 span。

来源:src/runtime/mgcsweep.go5-52 src/runtime/mheap.go57-99

内存结构与 GC

理解内存管理结构有助于理解 GC 的工作原理

关键结构

  • mheap: 中央堆管理器,维护 span 列表
  • mspan: 表示一个页面块,其中包含相同大小类的对象
  • heapBits: 指示内存中哪些字包含指针的位图
  • gcWork: GC 用于跟踪灰色对象的任务缓冲区

位图标记

  • 堆中的每个字在堆位图中都有相应的位
  • 用于跟踪哪些内存包含指针
  • GC 也使用它们来标记对象为可达
  • 大小对象有不同的标记算法

来源:src/runtime/mheap.go56-176 src/runtime/malloc.go17-61 src/runtime/mbitmap.go5-54

性能特征

暂停时间

  • 停止世界 (STW) 暂停通常在亚毫秒级别
  • GC 针对低延迟而非最大吞吐量进行了优化
  • 目标暂停时间可以通过 GOGC 进行调整

CPU 开销

  • 默认目标是使用 25% 的 CPU 进行 GC(由 gcBackgroundUtilization 控制)
  • 实际开销取决于分配率和活动对象集合的大小
  • 较高的 GOGC 值会降低 CPU 开销,但会增加内存使用量

内存开销

  • 垃圾收集器需要堆大小 >= 实时对象大小的 2 倍(默认 GOGC=100
  • GC 位图和工作队列会在堆中为每个字(word)增加大约 1 比特的内存开销

来源: src/runtime/mgcpacer.go15-45 src/runtime/mgc.go112-118

GC 调试和调优

环境变量

  • GOGC:设置垃圾收集目标百分比(默认值:100)
  • GOMEMLIMIT:设置 GC 将尝试遵守的软内存限制
  • GODEBUG=gctrace=1:将 GC 相关信息输出到 stderr

运行时控制

  • runtime.GC():强制进行垃圾回收
  • debug.SetGCPercent():在运行时更改 GC 目标百分比
  • runtime.ReadMemStats():获取内存分配统计信息

GC 指标和跟踪

  • runtime/metrics 包提供详细的 GC 指标
  • 执行跟踪器可以与 goroutine 执行一起可视化 GC 活动
  • 堆分析器可以帮助识别内存分配模式

来源: src/runtime/mgcpacer.go18-45 src/runtime/mgc.go464-486 src/runtime/export_test.go243-244

结论

Go 的垃圾收集器设计旨在提供可预测、低延迟的垃圾收集,并具有合理的内存开销。其并发设计允许应用程序代码在垃圾收集周期中以最小的干扰运行,使其适用于对延迟敏感的应用程序。

GC 的设计侧重于简单性和可预测性,而不是榨取最后一丁点的性能,这符合 Go 使编程更轻松、更富有成效同时仍然能提供良好性能的理念。

来源:src/runtime/mgc.go5-119 src/runtime/proc.go211-212