Go垃圾回收
Go垃圾回收
5、Golang三色标记混合写屏障GC模式全分析 (yuque.com)
垃圾回收的认识 | Go 程序员面试笔试宝典 (golang.design)
Go 语言垃圾收集器的实现原理 | Go 语言设计与实现 (draveness.me)
Go 语言的 GC 实现分析 (xargin.com)
Go 的 GC 目前使用的是无分代(对象没有代际之分)、不整理(回收过程中不对对象进行移动与整理)、并发(与用户代码并发执行)的三色标记清扫算法
只要一个变量还能被访问,它就不会被回收
Go 的编译器会通过逃逸分析将大部分新生对象存储在栈上(栈直接被回收),只有那些需要长期存在的对象才会被分配到需要进行垃圾回收的堆中
GC 简介
GC是一种自动内存管理的机制
当程序向操作系统申请的内存不再需要时,垃圾回收主动将其回收并供其他代码进行内存申请时候复用,或者将其归还给操作系统
这种针对内存级别资源的自动回收过程即为垃圾回收,负责垃圾回收的程序组件即为垃圾回收器
通常,垃圾回收器的执行过程被划分为两个半独立的组件:
- 赋值器:这一名称本质上是在指代用户态的代码,对垃圾回收器而言,用户态的代码仅仅只是在修改对象之间的引用关系,也就是在对象图(对象之间引用关系的一个有向图)上进行操作
- 回收器:负责执行垃圾回收的代码
根对象
根对象又称为根集合,是垃圾回收器在标记过程时最先检查的对象,包括:
- 全局变量:程序在编译期就能确定的那些存在于程序整个生命周期的变量
- 执行栈:每个 goroutine 都包含自己的执行栈,这些执行栈上包含栈上的变量及指向分配的堆内存区块的指针
- 寄存器:寄存器的值可能表示一个指针,参与计算的这些指针可能指向某些赋值器分配的堆内存区块
常见GC 方式
GC 算法可以归结为追踪和引用计数两种形式的混合运用
- 追踪式 GC
- 从根对象出发,根据对象之间的引用信息,一步步推进直到扫描完毕整个堆并确定需要保留的对象,从而回收所有可回收的对象
- Go、 Java、V8 对 JavaScript 的实现等均为追踪式 GC
- 引用计数式 GC
- 每个对象自身包含一个被引用的计数器,当计数器归零时自动得到回收。因为此方法缺陷较多,在追求高性能时通常不被应用
- Python、Objective-C 等均为引用计数式 GC
GC 触发时机
Go 语言中 GC 的触发时机存在两种形式:
- 主动触发,通过调用
runtime.GC来触发 GC,此调用阻塞式地等待当前 GC 运行完毕 - 被动触发,分为两种方式:
- 使用步调(Pacing)算法,其核心思想是控制内存增长的比例,默认配置会在堆内存达到上一次垃圾收集的 2 倍时,触发新一轮的垃圾收集
- 使用系统监控,当超过两分钟没有产生任何 GC 时,强制触发 GC
- 当前线程的内存管理单元中不存在空闲空间时,创建微对象和小对象需要从 MCentral 或者 MHeap 中获取新的管理单元,在这时就可能触发垃圾收集
- 当用户程序申请分配 32KB 以上的大对象时,一定会尝试触发垃圾收集
Go 的 GC 优化
三色标记
V1.3 标记清除算法
- 标记阶段:暂停程序(STW),从根节点的对象集合出发,查找并标记堆中所有存活的可达对象
- 清除阶段:遍历堆中所有的对象,回收未被标记的不可达对象,并将对应的内存加入空闲链表中
问题:
- STW(Stope The World)严重影响性能
- 清除数据产生堆内存碎片
全局变量:程序在编译期就能确定的那些存在于程序整个生命周期的变量
执行栈:每个 Goroutine 都包含自己的执行栈,执行栈上包含栈上变量及指向堆内存的指针
寄存器:寄存器的值可能表示一个指针,参与计算的这些指针可能指向堆内存
V1.5 三色标记法
三色标记算法将程序中的对象分为白色、灰色和黑色三类:
- 白色:潜在垃圾,未被回收器访问到的对象,在回收开始阶段,所有对象都被标记为白色;回收结束后,白色对象均不可达,释放相应内存
- 灰色:活跃对象,已被回收器访问到,但存在指向白色对象的外部指针,垃圾收集器需要继续扫描其子对象
- 黑色:活跃对象,已被垃圾收集器访问到,其所有字段都已被扫描
三色标记法步骤:
- 应用程序开始运行时,所有对象默认标记为白色
- 每次GC时从根节点遍历对象,将对象标记为灰色
- 对灰色对象集合中每个灰色对象,遍历其引用对象,将其引用的白色对象标记为灰色,之后将该灰色对象标记为黑色
- 重复遍历灰色对象集合直到集合为空,所有可达对象已标记为黑
- 回收仍为白色的不可达对象
不执行STW时,同时出现下面两个情况会破坏GC的正确性:
- 某个黑色对象引用白色对象
- 还未遍历到的灰色对象对白色对象的引用受到破坏
因此让 GC 满足以下两种情况之一,就能够保证正确性:
- 强三色不变性
不允许黑色对象引用白色对象的情况出现 - 弱三色不变性
允许黑色对象引用白色对象,但该白色对象必须同时可由另一个灰色对象通过若干其他白色对象到达
屏障机制
对象在内存槽中有两种位置:栈和堆,栈空间的特点是容量小,但要求响应速度快,由于栈上对象操作非常频繁,所以 Go 语言中写屏障机制仅在堆空间上启用
V1.7 Dijkstra 插入写屏障
操作:在对象A引用对象B的时候,将对象B标记为灰色,可满足强三色不变性
由于不对栈空间进行插入写屏障,因此在完成扫描标记后需要启用 STW,将所有栈对象标记为灰色并重新扫描栈空间(可能有栈上黑色对象引用栈或堆上白色对象的可能)
问题:减少了STW的时间,但仍然存在
Yuasa删除写屏障
操作:删除对于对象B的引用时,如果对象B为白色,将其标记为灰色,可满足弱三色不变性
问题:回收精度低,一次 GC 中一个对象即使被删除了最后一个引用也依旧可以存活,在下一轮 GC 中被清理掉
插入写屏障和删除写屏障的短板:
- 插入写屏障:结束时需要 STW 来重新扫描栈,标记栈上引用的白色对象的存活
- 删除写屏障:回收精度低,GC 开始时 STW 扫描堆栈来记录初始快照,这个过程会保护开始时刻的所有存活对象
V1.8 混合写屏障机制
操作:
- 对于栈,开始时扫描栈对象并将可达对象全部标记为黑色,GC 期间栈上创建的新对象均为黑色
- 对于堆,同时使用删除写屏障和插入写屏障,指针修改时,指向的新对象和修改前指向的旧对象都标记为灰色
可满足弱三色不变性,扫描阶段不需要 STW
V1.9
彻底移除了栈的重扫描过程,GC仅用于堆空间
因为栈上存在着根节点,堆上对象是通过根节点扫描到的
思考一下,我们在程序中一般创建对象,假设创建在堆上,然后我们在函数内去操作这个对象,这里我们是通过在函数中的局部变量去操作这个堆上的对象的,所以这种情况下堆上对象一定是和局部变量相关联的,局部变量是保存在栈上的,所以要找到所有堆中可达对象,栈上的对象可以作为根节点全部扫描一遍
V1.10 批量写屏障机制
将需要着色的指针统一写入一个缓存,每当缓存满时统一对缓存中的所有指针进行着色
辅助 GC
目前的 Go 实现中,当 GC 触发后,会首先进入并发标记的阶段。并发标记会设置一个标志,并在 mallocgc 调用时进行检查。当存在新的内存分配时,会暂停分配内存过快的那些 goroutine,并将其转去执行一些辅助标记(Mark Assist)的工作,从而达到放缓继续分配、辅助 GC 的标记工作的目的。
具体操作为编译器分析用户代码,并在需要分配内存的位置,将申请内存的操作翻译为 mallocgc 调用,而 mallocgc 的实现决定了标记辅助的实现,伪代码思路如下:
func mallocgc(t typ.Type, size uint64) {
if enableMarkAssist {
// 进行标记辅助,此时用户代码没有得到执行
(...)
}
// 执行内存分配
(...)
}
具体流程
当前流程:
| 阶段 | 说明 | 赋值器状态 |
|---|---|---|
| SweepTermination | 清扫终止阶段,为下一个阶段的并发标记做准备工作,启动写屏障 | STW |
| Mark | 扫描标记阶段,与赋值器并发执行,写屏障开启 | 并发 |
| MarkTermination | 标记终止阶段,保证一个周期内标记任务完成,停止写屏障 | STW |
| GCoff | 内存清扫阶段,将需要回收的内存归还到堆中,写屏障关闭 | 并发 |
| GCoff | 内存归还阶段,将过多的内存归还给操作系统,写屏障关闭 | 并发 |
GC 调优
垃圾回收的优化问题 | Go 程序员面试笔试宝典 (golang.design)
当我们谈论 GC 调优时,通常是指减少用户代码对 GC 产生的压力
一方面包含了减少用户代码分配内存的数量(即对程序的代码行为进行调优)
另一方面包含了最小化 Go 的 GC 对 CPU 的使用率(即调整 GOGC)
- 合理化内存分配的速度、提高赋值器的 CPU 利用率
- 降低并复用已经申请的内存
- 调整 GOGC
