本文基于 Go 1.13
Go 的垃圾回收器旨在帮助开发者自动清理应用程序的内存. 然而每次跟踪内存并清理都会影响程序运行的性能. Go 的垃圾回收器旨在清理内存的同时也关注性能, 主要是以下几个指标:
当程序暂停的时的两阶段尽可能减少 (这句我也不太知道怎么翻)
一次垃圾回收的周期少于 10ms
一次垃圾回收操作不能占用超过 25% 的 CPU
这看上去是一个很难实现的目标, 本篇文章就是介绍 Go 是如何完成这些目标的.
堆阈值 Heap Threshold Reached
垃圾回收器关注的第一个指标就是堆的增长. 默认情况下, 当堆的大小变成原来的两倍的时候, 垃圾回收器会被启动. 这里有个例子, 在循环里面不断分配内存
- func BenchmarkAllocationEveryMs(b *testing.B) {
- // need permanent allocation to clear see when the heap double its size
- var s *[]int
- tmp := make([]int, 1100000, 1100000)
- s = &tmp
- var a *[]int
- for i := 0; i <b.N; i++ {
- tmp := make([]int, 10000, 10000)
- a = &tmp
- time.Sleep(time.Millisecond)
- }
- _ = a
- runtime.KeepAlive(s)
- }
追踪曲线告诉我们, 垃圾回收器被触发
当堆的大小变成原来两倍的时候, 内存分配者会触发垃圾回收器. 这个也可以通过增加参数 GODEBUG=gctrace=1 来将整个生命周期的性能打印出来
- gc 8 @0.251s 0%: 0.004+0.11+0.003 ms clock, 0.036+0/0.10/0.15+0.028 ms CPU, 16->16->8 MB, 17 MB goal, 8 P
- gc 9 @0.389s 0%: 0.005+0.11+0.007 ms clock, 0.041+0/0.090/0.11+0.062 ms CPU, 16->16->8 MB, 17 MB goal, 8 P
- gc 10 @0.526s 0%: 0.046+0.24+0.014 ms clock, 0.37+0/0.14/0.23+0.11 ms CPU, 16->16->8 MB, 17 MB goal, 8 P
周期 9 是我们之前看到的运行时间为 389ms 的周期. 有趣的是这部分: 16->16->8 MB , 展示了在垃圾回收之前有多少内存正被占用以及垃圾回收之后剩余的内存量. 我们清楚地看到, 当周期 8 将堆减少到 8MB 时, 周期 9 已在 16MB 处触发.
这个阈值通过环境变量 GOGC 来设置, 默认是 100%, 也就是当堆的大小增加 100% 时垃圾回收器会被触发. 从性能原因考虑, 也为了避免不断地开始新的垃圾回收, 所以当堆的大小小于 4MB*GOGC 的时候, 尽管 GOGC 设成 100%, 但垃圾回收依然不会被触发
时间阈值 Time Threshold Reached
第二个垃圾回收器关注的之间是两次垃圾回收时间之间的间隔, 如果大于 2 分钟, 就会强制执行垃圾回收.
这个能根据给定 GODEBUG 参数看到, 程序在两分钟之后执行了强制的垃圾回收
- GC forced
- gc 15 @121.340s 0%: 0.058+1.2+0.015 ms clock, 0.46+0/2.0/4.1+0.12 ms CPU, 1->1->1 MB, 4 MB goal, 8 P
协助 Required Assistance
垃圾回收器由两部分组成
标记内存仍然在使用
将没有标记正在使用的内存进行替换
在标记阶段, Go 必须确保标记内存的速度比分配新内存的速度更快. 实际上, 如果收集器标记了 4Mb 的内存, 而在同一时间段内程序分配了相同数量的内存, 则垃圾收集器必须在完成后立即触发.
为了解除这个问题, Go 在标记内存的同时跟踪新的内存分配, 并且会去查看垃圾回收器什么时候需要被触发. 当垃圾回收触发时第一步开始, 他将首先准备给每个 processor(GMP 中的 P)一个 goroutine, 这个 gourtine 最开始是处理休眠状态的, 等待标记阶段的进行.
跟踪可以显示这些 goroutines
一旦这些 goroutinues 产生以后, 垃圾回收器会开始进行标记, 会去检查哪个变量是需要被收集以及替换的. 标记为 GC dedicated 的 goroutines 在没有抢占的情况下才会进行标记操作, 而标记为 GC 空闲的 goroutine 则在可以直接进行标记操作, 因为它们没有其他任何需要运行的东西, 可以被抢占.
垃圾回收器现在可以准备将变量标记为不再使用了. 对于每一个变量扫描, 都会增加一个 counter 为了跟踪当前工作还有多少剩余的工作需要被进行. 当在垃圾收集期间安排 goroutine 工作时, Go 会将所需的内存分配与已经完成的扫描进行比较, 以便比较扫描的速度和分配的要求. 如果扫描的速度能比分配的速度快则不需要额外的协助, 相反, 如果扫描的速度比内存分配的速度要慢, Go 会启动额外的 goroutine 来协助标记工作. 这个图反应了这个逻辑:
在我们的例子中, goroutine 14 被唤起工作当扫描速度比分配速度低的时候:
CPU 限制 CPU limitation
其中一个垃圾回收器的指标是不能占用超过 CPU 的 25%. 这意味着 Go 在标记阶段不能分配多于四分之一的处理器. 实际上, 这正是我们在前面的示例中看到的, 只有两个 goroutines 超出了处理器的高度, 完全专用于垃圾收集:
我们可以看到, 另一个 goroutine 在他没有其他工作的时候会为标记进行工作. 然而, 当垃圾回收器发出协助请求的时候, Go 会在高峰期时超过 25% 的 CPU 占用, 如我们所见 goroutinue 14
在我们的示例中, 在短时间内, 将 37.5%的处理器 (八分之三) 分配给标记阶段. 但这种情况可能很少见, 只有在内存高分配的情况下才会发生.
来源: http://www.tuicool.com/articles/ZZv6zam