26 go语言(golang) - GC原理
Go 语言的垃圾回收(Garbage Collection, GC)是一个重要的特性,它帮助自动管理内存,减少了内存泄漏和其他内存相关错误的可能性。Go 的 GC 是并发的,并且从 Go 1.5 版本开始,默认使用了一个并行的标记清除(Mark-Sweep)算法。
一、核心概念
-
并发标记-清除(Concurrent Mark and Sweep):
- Go 使用了一种并发标记-清除算法,这是一种非分代、非压缩的垃圾回收策略。
- 标记阶段:遍历所有可达对象,并将其标记为“活跃”。
- 清除阶段:扫描堆中的所有对象,释放那些未被标记为活跃的对象。
-
三色标记算法:
Go 使用三色标记算法进行垃圾回收。在这种方法中,对象被分为三种颜色:- 白色:表示对象可能是垃圾。
- 灰色:表示对象不是垃圾,但其引用需要进一步检查。
- 黑色:表示对象及其所有引用都已经被检查过,不是垃圾。
-
写屏障(Write Barrier):
在标记阶段期间,程序还在运行,并可能会修改对象之间的引用关系。为了处理这种情况,Go 使用写屏障来记录任何对堆上数据结构的修改。这确保即使在并发执行时也能正确地维护 GC 的数据一致性。 -
并发和低延迟:
Go 的 GC 设计目标之一是减少停顿时间(STW, Stop-The-World),即使在大型程序中也能保持较低延迟。GC 大部分工作是与应用程序并发执行的,只有在扫描栈空间时才需要短暂停顿。 -
GC 触发条件:
- 堆内存增长到达某个阈值。
- 手动调用
runtime.GC()
函数。 - 内存分配频率和数量等因素也会影响触发时机。
二、工作流程
2.1 初始化阶段
-
根对象识别
- GC 开始时,垃圾回收器从根集合开始扫描,这些根包括全局变量、栈上的变量以及寄存器中的指针。这些被视为初始可达对象。
-
颜色初始化
- 所有活动根开始作为灰色,表示它们需要被扫描。堆上其他非活动根的对象则开始为白色。
2.2 标记阶段
标记阶段是一个递归过程,用于遍历和标记所有可达对象
-
灰色处理
- 从灰色集合中取出一个灰度节点,将其变为黑,并将它所引用的所有白节点变成灰。
- 将这些新变成灰色的节点加入到待处理队列中。
-
并发执行
- 标记过程与程序执行同时进行,通过使用写屏障来确保在此过程中对堆内存进行修改时不会导致不一致性。
-
写屏障机制
- 写屏障用于捕获对堆内存的更新操作。当程序修改某个指针时,写屏障会确保任何新创建或更新引用关系导致的新可达性变化能够正确反映到颜色转换上,使得新创建或更新后的节点不会丢失。
2.3 清除阶段
一旦没有更多的灰色节点需要处理,标记阶段结束,进入清除阶段
- 释放不可达对象
- 遍历整个堆空间,将仍然是白色(即未被访问过)的对象视为不可达,从而可以安全地释放这些内存空间。
2.4 在哪些阶段会触发 STW?
STW 就是 Stop The World
- 标记开始(Mark Start)
- 在标记阶段开始之前,会触发一次 STW。这是为了确保所有线程都处于一致状态,以便准确识别和处理根对象。
- 在这个短暂的暂停期间,GC 会扫描栈、全局变量和 CPU 寄存器中的指针,将它们作为初始灰色对象。
- 标记终止(Mark Termination)
- 标记阶段结束时,再次触发 STW。这次暂停用于确保所有并发标记任务已经完成,并且没有遗漏任何需要处理的灰色节点。
- 此外,这个阶段还用于清理写屏障缓冲区,以保证后续清除操作的一致性。
- 清除准备(Sweep Termination):
- 虽然大部分清除工作可以并发进行,但在某些情况下,为了准备下一轮 GC 或者进行一些必要的数据结构更新,也可能会有一个短暂的 STW。
2.5 写屏障的重要性
假定标记完成的瞬间,A对象是黑色,B是白色,然后A的对象指针字段f
由空指针改成指向B,若没有写屏障的话,清除阶段B就会被清除掉,那边A的f
字段就变成了悬浮指针。
若存在写屏障那么f
字段改变的时候,f
指向的 B 就会放入到灰色集合中,然后继续扫描,B最终也会变成黑色的,那么清除阶段它也就不会被清除了。
-
并发垃圾回收支持
- 为了在程序运行时进行垃圾回收而不影响正常操作,Go 使用写屏障来维护三色不变性
-
动态更新状态变化
- 当堆中的指针发生修改时(如例子所述),写屏障会自动将新引用目标(如 B)置入灰集以确保后续能够正确追踪到相关数据依赖关系
-
避免悬浮指针问题
- 如例子中提到,如果 A 的字段
f
被修改以指向 B,而没有使用写屏障,那么在清除阶段可能会错误地释放 B,从而导致 A.f
成为悬浮指针; - 写屏障通过及时更新状态信息有效防止此类问题发生
- 如例子中提到,如果 A 的字段
三、特性与优化
-
并发执行
- GC 在后台运行,与应用程序同时进行,这样可以避免长时间 STW 事件。
-
增量式收集
- 通过分批次地完成标记和清理工作,而不是一次性全部完成,以减少每次暂停时间。
-
GOGC 调整
GOGC
是一个环境变量,用于控制触发垃圾回收的频率。默认值为100,即当堆增长到上次 GC 后大小的一倍时触发下一次 GC。- 增大
GOGC
值可以减少 GC 次数,提高吞吐量,但可能增加峰值延迟;减小此值则相反。
-
写屏障技术:
- 在进行赋值操作时会插入特殊代码逻辑以确保正确维护
- 有助于在并行执行过程中保证数据一致性
-
后台任务协作
- 部分工作交由专门后台线程完成,如定期检查是否需要启动新一轮循环等;
-
三色不变性原则
- 在整个过程中,不允许存在从黑到白直接相连的指针路径,以确保不会误删除仍然可达的数据;
四、版本更新
在 Go 1.5 版本之前,Go 使用的是一个停止世界(Stop-The-World, STW)标记清扫(Mark-Sweep)垃圾回收算法。这种早期的 GC 实现有几个显著的特点和限制
4.1 停止世界 (STW)
在早期版本中,垃圾回收过程会导致整个程序暂停执行。这意味着所有的 goroutine 都会在 GC 开始时停止运行,直到垃圾回收完成。这种全面暂停对于实时性或高并发要求较高的应用来说是一个较大问题,因为它可能导致明显的延迟波动。
4.2 标记清扫 (Mark-Sweep)
标记清扫算法主要分为两个阶段:
- 标记阶段:遍历所有活跃对象,并将它们标记为活跃状态。
- 清扫阶段:遍历堆内存中所有对象,释放未被标记为活跃的对象所占用的内存。
这种方法相对简单直观,但效率不高且完全暂停应用程序来执行垃圾回收会导致应用性能受到影响。
4.3 单线程 GC
在 Go 1.5 之前,GC 主要是单线程执行的。虽然 Go 程序本身可以高度并行运行多个 goroutine,但垃圾回收器自身并没有利用多核处理器优势来并行处理垃圾回收任务。这限制了 GC 的效率和速度。
4.4 改进和转变
随着 Go 1.5 的发布,在此版本中引入了并发三色标记算法,并开始利用多核心优势进行并发垃圾回收处理。
从此版本开始
- 引入了写屏障(write barrier),允许程序在进行堆内存写操作时继续运行。
- 增加了更多与调度器集成深度更深、更智能化管理内存分配和GC触发机制。