Go垃圾回收

Go垃圾回收

5、Golang三色标记混合写屏障GC模式全分析 (yuque.com)
垃圾回收的认识 | Go 程序员面试笔试宝典 (golang.design)
Go 语言垃圾收集器的实现原理 | Go 语言设计与实现 (draveness.me)
Go 语言的 GC 实现分析 (xargin.com)

Go 的 GC 目前使用的是无分代(对象没有代际之分)、不整理(回收过程中不对对象进行移动与整理)、并发(与用户代码并发执行)的三色标记清扫算法

只要一个变量还能被访问,它就不会被回收

Go 的编译器会通过逃逸分析将大部分新生对象存储在栈上(栈直接被回收),只有那些需要长期存在的对象才会被分配到需要进行垃圾回收的堆中

GC 简介

垃圾回收的认识 | Go 程序员面试笔试宝典 (golang.design)

GC是一种自动内存管理的机制

当程序向操作系统申请的内存不再需要时,垃圾回收主动将其回收并供其他代码进行内存申请时候复用,或者将其归还给操作系统
这种针对内存级别资源的自动回收过程即为垃圾回收,负责垃圾回收的程序组件即为垃圾回收器

通常,垃圾回收器的执行过程被划分为两个半独立的组件:

根对象

根对象又称为根集合,是垃圾回收器在标记过程时最先检查的对象,包括:

常见GC 方式

GC 算法可以归结为追踪和引用计数两种形式的混合运用

GC 触发时机

垃圾回收机制的实现 | Go 程序员面试笔试宝典 (golang.design)

Go 语言中 GC 的触发时机存在两种形式:

Go 的 GC 优化

三色标记

V1.3 标记清除算法

A
A
G
G
B
B
C
C
D
D
E
E
F
F
Root
Root
Text is not SVG - cannot display

问题

根集合

全局变量:程序在编译期就能确定的那些存在于程序整个生命周期的变量
执行栈:每个 Goroutine 都包含自己的执行栈,执行栈上包含栈上变量及指向堆内存的指针
寄存器:寄存器的值可能表示一个指针,参与计算的这些指针可能指向堆内存

V1.5 三色标记法

三色标记算法将程序中的对象分为白色、灰色和黑色三类:

三色标记法步骤

  1. 应用程序开始运行时,所有对象默认标记为白色
  2. 每次GC时从根节点遍历对象,将对象标记为灰色
  3. 对灰色对象集合中每个灰色对象,遍历其引用对象,将其引用的白色对象标记为灰色,之后将该灰色对象标记为黑色
  4. 重复遍历灰色对象集合直到集合为空,所有可达对象已标记为黑
  5. 回收仍为白色的不可达对象

不执行STW时,同时出现下面两个情况会破坏GC的正确性

因此让 GC 满足以下两种情况之一,就能够保证正确性:

屏障机制

再谈 Golang 垃圾回收 – 兰陵美酒郁金香的个人博客 (xhyonline.com)

对象在内存槽中有两种位置:栈和堆,栈空间的特点是容量小,但要求响应速度快,由于栈上对象操作非常频繁,所以 Go 语言中写屏障机制仅在堆空间上启用

V1.7 Dijkstra 插入写屏障

操作:在对象A引用对象B的时候,将对象B标记为灰色,可满足强三色不变性

由于不对栈空间进行插入写屏障,因此在完成扫描标记后需要启用 STW,将所有栈对象标记为灰色并重新扫描栈空间(可能有栈上黑色对象引用栈或堆上白色对象的可能)

问题:减少了STW的时间,但仍然存在

Yuasa删除写屏障

操作:删除对于对象B的引用时,如果对象B为白色,将其标记为灰色,可满足弱三色不变性

问题:回收精度低,一次 GC 中一个对象即使被删除了最后一个引用也依旧可以存活,在下一轮 GC 中被清理掉

插入写屏障和删除写屏障的短板:

V1.8 混合写屏障机制

操作:

可满足弱三色不变性,扫描阶段不需要 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 {
		// 进行标记辅助,此时用户代码没有得到执行
		(...)
	}
	// 执行内存分配
	(...)
}

具体流程

gc-process.png (516×349) (golang.design)

当前流程:

阶段 说明 赋值器状态
SweepTermination 清扫终止阶段,为下一个阶段的并发标记做准备工作,启动写屏障 STW
Mark 扫描标记阶段,与赋值器并发执行,写屏障开启 并发
MarkTermination 标记终止阶段,保证一个周期内标记任务完成,停止写屏障 STW
GCoff 内存清扫阶段,将需要回收的内存归还到堆中,写屏障关闭 并发
GCoff 内存归还阶段,将过多的内存归还给操作系统,写屏障关闭 并发

GC 调优

垃圾回收的优化问题 | Go 程序员面试笔试宝典 (golang.design)

当我们谈论 GC 调优时,通常是指减少用户代码对 GC 产生的压力
一方面包含了减少用户代码分配内存的数量(即对程序的代码行为进行调优)
另一方面包含了最小化 Go 的 GC 对 CPU 的使用率(即调整 GOGC)