Golang GC发展历程
Go 1.0(2012年)
- 算法:简单的 标记-清除(Mark-Sweep),非并发(STW, Stop-The-World)。
Go 1.1(2013年)
- 改进:引入 并行标记(Parallel Mark) 和 增量式清除(Incremental Sweep)。
Go 1.3(2014年)
- 改进:完全精确的堆栈扫描(Precise Stack Scanning)。
- 特点:解决了保守式栈扫描导致的“内存泄漏”问题(误将数字当作指针,导致对象无法回收)。
Go 1.4(2014年)
- 改进:运行时代码用 Go 重写(之前部分为 C/C++)。
Go 1.5(2015年)—— 重大突破
- 算法:引入 并发标记-清除(Concurrent Mark-Sweep)。
- 核心改进:
- 三色标记法(Tri-Color Marking):实现并发标记。
- 写屏障(Write Barrier):在并发标记期间维护对象图的正确性。
- STW 阶段大幅缩短:从毫秒级降至 毫秒以下(通常 1ms 以内)。
Go 1.8(2017年)–Go GC 低延迟的标志性版本
- 改进:混合写屏障(Hybrid Write Barrier)。
- 特点:
- 结合插入写屏障和删除写屏障,减少 STW 时间。
- STW 时间降至 1 毫秒以下,甚至达到 微秒级。
Go 1.9 ~ 1.12(2017-2019年)
- 持续优化:
- 减少内存碎片(如重用内存块)。
- 优化大对象分配(Large Object Space)。
- 改进 GC pacing(动态调整触发阈值)。
Go 1.14(2020年)
- 改进:抢占式调度与 GC 协作。
- 特点:
- 解决“长耗时协程阻塞 GC”的问题。
- 进一步降低延迟,尤其是高负载场景。
Go 1.18+(2022年至今)
- 优化方向:
- 减少内存占用(如更智能的分配策略)。
- 针对泛型(Generics)引入后的 GC 适应性调整。
- 持续优化写屏障和扫描性能。
当前状态(Go 1.21+)
- 算法:并发三色标记-清除 + 混合写屏障。
STW是什么?
在上述编年史中,我们注意到STW这一陌生单词,STW是什么?这对后续的回收策略至关重要。
概念
STW(Stop-The-World) 是垃圾回收(GC)过程中的一种行为,指 暂停所有应用线程(或协程),以确保垃圾回收器能安全地分析内存对象引用关系。此时,整个程序的所有业务逻辑会暂时停止,直到 GC 完成关键阶段。
STW 是 GC 的“必要之恶”,是无法避免的,但可通过并发、写屏障等技术优化。
为什么需要STW
垃圾回收的核心任务是 识别并回收无用内存,但如果在回收过程中程序仍在修改对象引用(例如新增/删除指针),可能导致:
- 误回收:将存活对象当作垃圾回收(致命错误)。
- 内存泄漏:漏掉真正的垃圾对象。
STW 的本质是“冻结”程序状态,让 GC 在一致的内存快照下工作,避免上述问题。
GC中STW的触发时机
不同 GC 算法的 STW 行为不同,但通常发生在以下阶段:
GC 阶段 | 是否需要 STW? | 说明 |
---|---|---|
标记开始 | 是(短暂) | 暂停程序,扫描根对象(栈、全局变量等)以确定存活对象起点。 |
标记过程 | 取决于算法 | 并发标记(如 Go 1.5+)可减少 STW;非并发 GC(如 Go 1.0)全程 STW。 |
标记终止 | 是(短暂) | 处理剩余标记任务,确保所有存活对象被识别。 |
清除/回收 | 通常不需要 | 回收内存可与程序并发执行(如 Go 的并发清除)。 |
STW 的性能影响
- 延迟(Latency):STW 时间越长,程序卡顿越明显(如游戏、实时系统无法容忍)。
- 吞吐量(Throughput):频繁 STW 会降低程序整体执行效率。
示例:
- Go 1.0 的 STW 可能持续 数百毫秒,明显卡顿。
- Go 1.8+ 通过混合写屏障将 STW 优化到 微秒级(用户无感知)。
如何减少 STW?
现代 GC 的核心优化方向就是 最小化 STW,主要技术包括:
技术 | 原理 | 代表实现 |
---|---|---|
并发标记 | 标记阶段与程序并发执行,仅短暂 STW 扫描根对象。 | Go 1.5 的三色标记法 |
写屏障(Write Barrier) | 在程序修改指针时记录引用变化,辅助 GC 维护正确性。 | Go 的混合写屏障(Hybrid WB) |
增量式 GC | 将 GC 工作拆分为多个小步骤,交替执行 GC 和程序。 | Java 的增量 CMS |
分代 GC | 假设新对象更易消亡,优先回收年轻代,减少全局 STW。 | Java 的 G1、ZGC |
Go 语言中的 STW 优化
Go 的 GC 演进史就是一部 STW 缩减史:
- Go 1.0:全程 STW,延迟极高。
- Go 1.5:引入并发标记,STW 降至 1ms 以下。
- Go 1.8:混合写屏障将 STW 压到 微秒级。
- Go 1.14+:抢占式调度解决长耗时协程阻塞 GC 的问题。
常见的垃圾回收策略
可以从发展历程看到一开始是用的标记清除算法,我们需要了解有哪些GC策略
引用计数法Reference Counting
是最简单的垃圾回收算法,给对象添加一个引用字段,每当被引用时,计数+1;当引用失效,计数-1;当计数为0时,表示对象不再被使用,可以回收。
优点
- 无需遍历,容易查找
- 立即回收垃圾,无全局STW,一旦引用计数为0,立即将自身连接到空闲链表上,等待回收
- 最大限度减少程序暂停时间
缺点
- 无法解决循环引用问题
- 每次引用计数器发生变化都要修改计数器,引起额外开销
- 需要额外空间存储计数器
追踪回收算法Tracing Garbage Collection
追踪回收算法又分为三类:
- 标记-清除Mark-Sweep
- 标记-整理Mark-Compact
- 标记-复制Mark-Copying
以上三种都需要STW,暂停程序运行
标记-清除( Mark-Sweep)
核心流程
- 标记阶段:从根对象(栈、全局变量等)出发,递归标记所有可达对象。
- 清除阶段:遍历堆内存,回收未被标记的对象(垃圾)。
标记-整理(Mark-Compact)
核心流程
- 标记阶段:同标记-清除,识别存活对象。
- 整理阶段:将存活对象向内存一端移动,消除碎片。
标记-复制(Mark-Copying)
核心流程
- 标记阶段:同标记-清除,识别存活对象。
- 复制阶段:将存活对象复制到另一块内存区域,清空原区域。
三色标记法
基本原理
为了解决原始标记清除算法带来的长时间STW, Go从v1.5版本实现了基于三色标记清除的并发垃圾收集器,在不暂停程序的情况下即可完成对象的可达性分析,三色标记算法将程序中的对象分成白色、黑色和灰色三类:
- 白色对象 - 潜在的垃圾,表示还未搜索到的对象,其内存可能会被垃圾收集器回收;
- 黑色对象 - 活跃的对象,表示搜索完成的对象,包括不存在任何引用外部指针的对象以及从根对象可达的对象
- 灰色对象 - 活跃的对象,表示正在搜索还未搜索完的对象,因为存在指向白色对象的外部指针,垃圾收集器会扫描这些对象的子对象;
三色标记法属于增量式GC算法,回收器首先将所有对象标记成白色,然后从gc root出发,逐步把所有可达的对象变成灰色再到黑色,最终所有的白色对象都是不可达对象。
具体实现:
- 初始时所有对象都是白色的
- 从
gc root
对象出发,扫描所有可达对象标记为灰色,放入待处理队列 - 从队列取出一个灰色对象并标记为黑色,将其引用对象标记为灰色,放入队列
- 重复上一步骤3,直到灰色对象队列为空
- 此时剩下的所有白色对象都是垃圾对象
动态图示:
静态图示:
初始化,都是白色
从root对象出发,变为灰色(不是只有一个root)
从灰色队列中,取出元素变为黑色
重复操作,直到灰色队列为空
灰色已经为空,没有继续引用的对象
回收所有白色对象
三色不变性
想要在并发或者增量的标记算法中保证正确性,我们需要达成一下两种三色不变性中的任意一种。
- 强三色不变性——黑色对象不会指向白色对象,只会指向灰色对象或者黑色对象。
- 如果满足这条,白色对象一旦被黑色对象引用,必须立即通过写屏障(Write Barrier)将其标记为灰色。
- 这是保守但安全的策略(如 Go 的早期 GC 实现)。
- 弱三色不变性——黑色对象指向的白色对象必须包含一条从灰色对象经由多个白色对象的可达路径。
- 通过写屏障捕获新增的引用关系,并在最终标记阶段(STW)重新扫描这些引用。
- 现代 GC(如 Go 的并发标记)通常采用这种优化策略。
屏障技术
屏障技术(如 写屏障,Write Barrier)是垃圾回收器的核心机制,用于在 并发标记阶段 确保对象引用关系变化时,GC 仍能正确识别存活对象,避免漏标或误回收。
1. 为什么需要屏障技术?
在 并发垃圾回收(GC 和用户程序同时运行)时,用户代码可能会修改对象的引用关系,导致 GC 标记与实际内存状态不一致。例如:
// 初始状态:A(黑) -> B(白)
A.Next = B // A 是已标记的黑色对象,B 是未标记的白色对象
A.Next = C // 用户程序修改引用,A -> C
如果没有屏障:
- GC 已经扫描完
A
(黑色),不会重新检查它的引用。 B
仍然是白色,可能被错误回收(即使它仍被其他对象引用)。
屏障的作用就是捕获这类修改,确保 GC 的正确性。
2. 屏障的类型
(1)写屏障(Write Barrier)
- 适用场景:并发标记阶段(如 Go 的三色标记法)。
- 功能:在用户程序 修改指针 时,拦截写入操作,并通知 GC 处理。
- Go 的实现(混合写屏障,Hybrid Barrier):
- 插入写屏障(Insertion Barrier):如果黑色对象引用白色对象,将白色对象标记为灰色。
- 删除写屏障(Deletion Barrier):如果删除引用关系,保留旧引用的标记状态。
示例:
// 用户代码:A(黑) -> B(白)
A.Next = B // 写屏障介入,将 B 标记为灰色
(2)读屏障(Read Barrier)
- 适用场景:某些增量式 GC(如 Java 的 ZGC)。
- 功能:在读取指针时检查对象状态(较少使用,因性能开销大)。
3. 屏障如何解决 GC 问题?
问题1:黑色对象引用白色对象(漏标)
- 无屏障:黑色对象不会重新扫描,白色对象可能被误回收。
- 写屏障解决方案:
- 强不变式:直接禁止黑色→白色的引用(立即将白色变灰)。
- 弱不变式:记录引用,后续重新扫描(如 Go 的混合屏障)。
问题2:并发修改导致浮动垃圾
- 无屏障:已删除的引用可能导致对象无法被正确回收。
- 写屏障解决方案:
- 删除写屏障保留旧引用的标记状态,确保对象不会提前被回收。
4. Go 的混合写屏障(Hybrid Write Barrier)
Go 1.8+ 使用混合写屏障,结合了 插入写屏障 + 删除写屏障 的优点:
- 插入写屏障:处理新引用(
A -> B
),确保白色对象被标记。 - 删除写屏障:处理引用删除(
A -> B
改为A -> C
),防止旧引用影响标记。
优势:
- 减少 STW 时间(Go 的 STW 通常在 1ms 以内)。
- 支持高并发,适合大规模内存管理。
5. 屏障对性能的影响
- 额外开销:每次指针写入都会触发屏障逻辑(少量 CPU 开销)。
- 优化手段:
- 编译器优化:对栈上的写入跳过屏障(栈对象由 STW 保证)。
- 批量处理:合并屏障操作,减少原子操作。
6. 总结
技术 | 作用 | 适用场景 |
---|---|---|
写屏障 | 拦截指针写入,防止漏标 | Go 的并发标记 |
读屏障 | 拦截指针读取,检查对象状态 | ZGC、Shenandoah |
混合屏障 | 结合插入+删除屏障 | Go 1.8+ |
屏障的核心目标:在 不暂停用户程序(或极短暂停) 的情况下,保证 GC 正确识别所有存活对象。