Go 语言的垃圾回收(Garbage Collection,简称 GC)是其自动内存管理的核心机制,旨在自动识别并回收不再被使用的内存,避免内存泄漏,减轻开发者的手动内存管理负担。Go 的 GC 算法经历了多次迭代优化,目前采用的是并发标记 - 清除(Concurrent Mark and Sweep) 算法,并结合了多种优化技术,以实现低延迟、高吞吐量的目标。
一、Go 垃圾回收的核心目标
Go 的 GC 设计围绕以下核心目标展开:
- 低延迟:尽可能减少 GC 对程序运行的阻塞时间(Stop-The-World,简称 STW),避免影响用户程序的响应速度。
- 高吞吐量:在保证低延迟的同时,高效地回收内存,减少 GC 本身的性能开销。
- 简单高效:算法实现简洁,适配 Go 语言的并发模型(如 goroutine)和内存分配机制。
二、Go 垃圾回收的核心算法:并发标记 - 清除(Concurrent Mark and Sweep)
Go 的 GC 基于追踪式垃圾回收(通过追踪对象的引用关系判断是否存活),核心流程分为标记(Mark) 和清除(Sweep) 两个阶段,且大部分工作在用户程序运行的同时(并发)执行,仅在关键节点需要短暂 STW。
[初始标记] → [并发标记] → [重新标记] → [并发清除](STW) (并发) (STW) (并发)
1. 标记阶段(Mark Phase)
标记阶段的目标是识别出所有存活的对象(被当前程序直接或间接引用的对象),具体流程如下:
初始标记(Initial Mark,STW):
- 这是标记阶段的第一个步骤,需要短暂 STW(通常只有几微秒到几十微秒)。
- GC 会暂停所有用户 goroutine,从根对象(Root Set)开始标记直接引用的对象。根对象包括:
- 全局变量(如包级变量);
- 当前运行的 goroutine 的栈内存(局部变量、函数参数等);
- 寄存器中的引用(暂存的变量指针)。
- 初始标记完成后,立即恢复用户程序运行。
并发标记(Concurrent Mark):
- 初始标记后,用户程序继续运行,GC 同时在后台(由专门的 GC 协程)进行标记。
- 后台 GC 协程会从初始标记的对象出发,递归遍历所有可达的对象(通过对象的引用指针),并标记为 “存活”。
- 由于用户程序在运行时可能修改对象的引用关系(如新增、删除指针),Go 采用写屏障(Write Barrier) 技术跟踪这些修改,确保标记的准确性:
- 写屏障会在用户程序修改指针时(如
a = &b
)触发,记录指针的变化,防止并发标记时遗漏或误判对象的存活状态。
- 写屏障会在用户程序修改指针时(如
重新标记(Remark,STW):
- 并发标记结束后,需要再次短暂 STW,处理并发标记期间因用户程序修改引用而产生的 “漏标” 对象(称为 “浮动垃圾” 的一部分)。
- 重新标记会利用写屏障记录的指针变化,快速修正标记结果,确保所有存活对象都被标记。
2. 清除阶段(Sweep Phase)
清除阶段的目标是回收未被标记的对象(即垃圾)所占用的内存,具体流程如下:
- 并发清除(Concurrent Sweep):
- 清除阶段完全并发执行,不阻塞用户程序。
- GC 会遍历堆内存,释放所有未被标记的对象(垃圾),并将其内存块归还给空闲内存池,供后续内存分配使用。
- 清除过程中,若用户程序需要分配新内存,会优先复用已回收的空闲内存,减少内存碎片。
三、Go GC 的关键优化技术
为实现低延迟和高吞吐量,Go 引入了多项优化技术:
1. 写屏障(Write Barrier)
- 作用:在并发标记阶段,跟踪用户程序对指针的修改,防止因引用关系变化导致的标记错误。
- 实现:Go 使用Dijkstra 写屏障(早期版本)和混合写屏障(Go 1.8 及以后,结合了 Dijkstra 和 Yuasa 写屏障的优势),在指针赋值时插入额外逻辑,记录旧指针和新指针的信息,确保并发标记的准确性。
- Dijkstra 写屏障:当修改指针时,将旧指针指向的对象标记为灰色(防止旧对象被漏标)
- Yuasa 写屏障:当修改指针时,将新指针指向的对象标记为灰色(防止新对象被漏标)
// 伪代码:指针赋值时触发的写屏障 func writeBarrier(old *T, new *T) {// 1. 将旧指针指向的对象标记为灰色(Dijkstra 逻辑)gray(old)// 2. 将新指针指向的对象标记为灰色(Yuasa 逻辑)gray(new) }
2. 三色标记法(Tri-Color Marking)
- 标记阶段通过 “三色” 区分对象的状态,简化并发标记的管理:
- 白色:未被标记的对象(初始状态,可能是垃圾);
- 灰色:已被标记,但引用的子对象尚未遍历(待处理);
- 黑色:已被标记,且所有子对象都已遍历(确定存活)。
- 流程:初始标记将根对象设为灰色,并发标记时将灰色对象的子对象标记为灰色、自身设为黑色,最终所有黑色对象为存活,白色为垃圾。
3. 增量 GC 与并发执行
- Go 的 GC 不是一次性完成所有工作,而是将标记和清除阶段拆分为可中断、可并发的步骤,大部分工作与用户程序并行执行,仅在初始标记和重新标记阶段短暂 STW,大幅降低了对程序运行的影响。
4. 内存分代(Generational GC)的部分实现
- 虽然 Go 没有严格的 “分代” 设计(如 Java 的新生代、老年代),但通过对象年龄判断(如频繁分配和回收的小对象更可能被优先处理)和内存块大小分类(如 tiny、small、large 分配器),优化了 GC 对短期对象的回收效率。
5. 自适应触发机制
- GC 触发时机并非固定,而是根据内存分配速率、堆内存大小等动态调整:
- 当堆内存增长到上次 GC 后堆大小的 2 倍(可通过
GOGC
环境变量调整,默认 100)时触发; - 若内存分配速率过快,也会提前触发,避免内存溢出。
- 当堆内存增长到上次 GC 后堆大小的 2 倍(可通过
三、Go GC 的演进历程
Go 的 GC 算法经过多次重大优化,核心版本演进如下:
- Go 1.0:采用简单的标记 - 清除算法,STW 时间长(毫秒级)。
- Go 1.5:引入并发标记 - 清除,STW 时间缩短至百微秒级。
- Go 1.8:使用混合写屏障,进一步减少 STW 时间(通常低于 100 微秒)。
- Go 1.12+:优化了重新标记阶段的效率,引入更多并发优化,使 STW 时间可稳定在微秒级。