当文中提及目前、目前版本等字眼时均指 Go 1.14,此外,文中所有 go 命令版本均为 Go 1.14。
GC
,全称 Garbage Collection
,即垃圾回收,是一种自动内存管理的机制。STW
可以是 Stop the World
的缩写,也可以是 Start the World
的缩写。通常意义上指指代从 Stop the World
这一动作发生时到 Start the World
这一动作发生时这一段时间间隔,即万物静止。STW 在垃圾回收过程中为了保证实现的正确性、防止无止境的内存增长等问题而不可避免的需要停止赋值器进一步操作对象图的一段过程。STW
越长,对用户代码造成的影响(例如延迟)就越大,早期 Go 对垃圾回收器的实现中 STW
长达几百毫秒,对时间敏感的实时通信等应用程序会造成巨大的影响。我们来看一个例子:OK
,其罪魁祸首是进入 STW 这一操作的执行无限制的被延长。for {}
所在的 goroutine 永远都不会被中断,从而始终无法进入 STW 阶段。实际实践中也是如此,当程序的某个 goroutine 长时间得不到停止,强行拖慢进入 STW 的时机,这种情况下造成的影响(卡死)是非常可怕的。好在自 Go 1.14 之后,这类 goroutine 能够被异步地抢占,从而使得进入 STW 的时间不会超过抢占信号触发的周期,程序也不会因为仅仅等待一个 goroutine 的停止而停顿在进入 STW 之前的操作上。GODEBUG=gctrace=1
wall clock 是指开始执行到完成所经历的实际时间,包括其他程序和本程序所消耗的时间; cpu time 是指特定程序使用 CPU 的时间; 他们存在以下关系:
wall clock < cpu time: 充分利用多核 wall clock ≈ cpu time: 未并行执行 wall clock > cpu time: 多核优势不明显
go tool trace
go tool trace
的主要功能是将统计而来的信息以一种可视化的方式展示给用户。要使用此工具,可以通过调用 trace API:debug.ReadGCStats
runtime.ReadMemStats
debug.GCStats
[2] 和 runtime.MemStats
[3] 的字段,这里不再赘述。trace.out
文件,我们可以使用 go tool trace trace.out
命令得到下图:三色抽象
的情况下,回收可以正常结束。但是并发回收的根本问题在于,用户态代码在回收过程中会并发地更新对象图,从而造成赋值器和回收器可能对对象图的结构产生不同的认知。这时以一个固定的三色波面作为回收过程前进的边界则不再合理。A.ref1
为 nil
,什么事情也没有发生scan(A)
什么也不会发生,进而 B 在此次回收过程中永远不会被标记为黑色,进而错误地被回收。C.ref3 = C.ref2.ref1
:赋值器并发地将黑色对象 C 指向(ref3)了白色对象 B;A.ref1 = nil
:移除灰色对象 A 对白色对象 B 的引用(ref2);*slot
可能会变为黑色,为了确保 ptr
不会在被赋值到 *slot
前变为白色,shade(ptr)
会先将指针 ptr
标记为灰色,进而避免了条件 1。如图所示:*slot
可能会变为黑色,为了确保 ptr
不会在被赋值到 *slot
前变为白色,shade(*slot)
会先将 *slot
标记为灰色,进而该写操作总是创造了一条灰色到灰色或者灰色到白色对象的路径,进而避免了条件 2。ptr
的着色还额外包含对执行栈的着色检查,但由于时间有限,并未完整实现过,所以混合写屏障在目前的实现伪代码是:ptr
指针进行着色。GOGC
或者 debug.SetGCPercent
进行控制(他们控制的是同一个变量,即堆的增长率 $\rho$)。整个算法的设计考虑的是优化问题:如果设上一次 GC 完成时,内存的数量为 $H_m$(heap marked),估计需要触发 GC 时的堆大小 $H_T$(heap trigger),使得完成 GC 时候的目标堆大小 $H_g$(heap goal) 与实际完成时候的堆大小 $H_a$(heap actual)最为接近,即: $\min |H_g - H_a| = \min|(1+\rho)H_m - H_a|$。求解这两个优化问题的具体数学建模过程我们不在此做深入讨论,有兴趣的读者可以参考两个设计文档:Go 1.5 concurrent garbage collector pacing[5] 和 Separate soft and hard heap size goal[6]。
gcpacertrace=1
):mallocgc
调用,而 mallocgc
的实现决定了标记辅助的实现,其伪代码思路如下:concat
函数负责拼接一些长度不确定的字符串。并且为了快速完成任务,出于某种原因,在两个嵌套的 for 循环中一口气创建了 800 个 goroutine。在 main 函数中,启动了一个 goroutine 并在程序结束前不断的触发 GC,并尝试输出 GC 的平均执行时间:/example2
的请求时,都会创建一段内存,并用于进行一些后续的工作。/debug/pprof/trace
路由来进行,其中 seconds
参数设置为 20s,并将 trace 的结果保存为 trace.out
:ab
,来同时产生 500 个请求 (-n
一共 500 个请求,-c
一个时刻执行请求的数量,每次 100 个并发请求):go tool pprof
来查看究竟是谁分配了大量内存(使用 web 指令来使用浏览器打开统计信息的可视化图形):newBuf
产生的申请的内存过多,现在我们使用 sync.Pool 来复用 newBuf
所产生的对象: