GC

Golang GC 的触发时机

Golang 的 GC 有两种触发方式:

  • 主动触发: 通过调用 runtime.GC() 函数来手动触发 GC。这种方式会阻塞当前程序,直到 GC 完成。
  • 被动触发: Golang 的 GC 会根据以下两种机制自动触发:
  • 内存使用阈值触发: 当堆内存使用量达到上次 GC 后存活对象内存的 2 倍时,会触发新一轮 GC。这个阈值可以通过环境变量 GOGC 来调整。
  • 定时触发: 如果一段时间(默认 2 分钟)内没有触发过 GC,运行时会强制触发一次 GC。

Golang GC 的实现原理

Golang 的 GC 采用了三色标记清除算法:

  1. 标记阶段:
    • 将所有根对象(全局变量、栈上对象等)标记为"灰色"并入队。
    • 从灰色对象出发,递归地将其引用的对象标记为"黑色"。
    • 在标记过程中,如果发现新的对象被引用,会将其标记为"灰色"并入队。
  2. 清除阶段:
    • 将所有未被标记为"黑色"的对象(即"白色"对象)回收。
    • 将回收的内存归还给操作系统。
    • Golang 的 GC 实现中还引入了写屏障(write barrier)机制,用于在标记阶段检测对象引用关系的变化,确保标记的正确性。

总的来说,Golang 的 GC 机制通过自动触发和三色标记清除算法,能够高效地管理内存,减少内存碎片,提高程序的性能和稳定性。

什么时候触发 STW

  • 标记准备阶段(Mark Setup):

时机:GC 周期开始时
目的: 启用写屏障
准备 GC 工作线程
扫描所有 goroutine 的栈
持续时间:通常在 10-30 微秒级别

  • 标记终止阶段(Mark Termination):

时机:并发标记阶段结束后
目的: 重新扫描(Rescan)全局队列和 P 本地队列中的对象
执行一些清理操作
关闭写屏障
持续时间:通常比标记准备阶段稍长,但仍在毫秒级别以下

非 GC 周期触发的 STW

  • 全局内存回收:
    时机:当需要将内存返还给操作系统时
    目的:确保安全地释放不再使用的内存页
    频率:相对较低,通常在程序空闲时进行

  • 栈收缩(Stack Shrinking):
    时机:在 GC 周期结束后,如果发现某些 goroutine 的栈使用率低
    目的:减小栈的大小,释放未使用的内存
    注意:这个操作通常与 GC 结合进行,以减少额外的 STW

  • 大对象分配:
    时机:当程序尝试分配一个非常大的对象时
    目的:确保有足够的连续内存空间
    频率:相对较低,取决于程序的内存使用模式

STW 的触发机制

协作式抢占:Go 1.14 之前,STW 依赖于 goroutine 在特定检查点(如函数调用)自愿让出控制权 异步抢占:Go 1.14 及之后,引入了基于信号的异步抢占机制,使 STW 更加及时和可预测

减少 STW 影响的策略

  • 增量 GC: Go 1.5 之后引入,将 GC 工作分散到多个小的增量中
    减少了单次 STW 的持续时间

  • 并行 GC: 利用多核并行执行 GC 工作
    显著减少了 STW 的总时间

  • 后台 GC: 在程序空闲时执行部分 GC 工作
    减少了 GC 对程序性能的影响

STW 持续时间的演进
Go 1.5 之前:STW 时间可能达到几百毫秒
Go 1.5 - 1.12:STW 时间显著减少,通常在 10 毫秒以下
Go 1.13+:大多数情况下,STW 时间控制在 100 微秒量级

监控和调优
使用 GODEBUG=gctrace=1 环境变量可以查看详细的 GC 日志,包括 STW 时间 通过 pprof 工具分析 GC 性能 调整 GOGC 值可以影响 GC 频率,间接影响 STW 频率