大家好,我是 Okada([@ocadaruma](https://twitter.com/ocadaruma)),LINE 广告平台团队的成员。我对 Go 的 GC (垃圾收集器)有点感兴趣,甚至还促使我专门写一篇关于它的博客。Go 是一门由 Google 开发,并且支持垃圾收集的编程语言。Go 通过[管道](https://tour.golang.org/concurrency/2) 支持并发。很多的公司,包括 Google,都在使用 Go,LINE 也用 Go 来开发工具和服务。
## Go GC
用 Go,你可以很容易地创建出低延时的应用。Go GC 似乎比其他语言的运行时要简单得多。对于 [Go 1.10](https://golang.org/doc/go1.10) 版本,它的垃圾收集器是 Concurrent Mask & Sweep (CMS) ,它不是压缩的,也不是分代的。这一点跟 JVM 不同。
- 它是一个,并行标记,用一个写屏障(写的时候阻塞)的清理(程序)。它是非分代,非压缩的。 -- [mgc.go](https://github.com/golang/go/blob/4d7cf3fedbc382215df5ff6167ee9782a9cc9375/src/runtime/mgc.go)
下面是 Java GC 和 Go GC 的对比。相比于 Java ,Go GC 对于我来说看起来有点简单,所以我决定深入进去,看下 Go GC 是怎么工作的。
| | Java(Java8 HotSpot VM) | Go
| :- | :- | :- |
| Collector | Several collectors (Serial, Parallel, CMS, G1) | CMS |
| Compaction | Compacts | Does not compact |
| Generational GC | Generational GC | Non-generational GC |
| Tuning parameters | Depends on the collector. Multiple parameters available. | GOGC only |
## 压缩(Compaction)
垃圾收集可以选择不迁移或者迁移(堆上的对象)。
## 不迁移的 GC(Non-moving GC)
不迁移的垃圾收集不会在堆中给对象重新分配内存。CMS,Go 使用的收集器,就是非迁移的。一般来说,如果你在非迁移的垃圾收集器中,重复地进行内存分配跟释放,最终将导致堆碎片,从而降低分配(堆内存)的性能。但,当然,这也取决于你的内存分配器如何实现。
![](https://engineering.linecorp.com/image/2018/8/2/1533208430578.png)
## 迁移的 GC(moving GC)
移动垃圾收集器将活动对象移动到堆的末尾来压缩堆。移动垃圾收集器的一个实例是拷贝 GC(Copying GC),它在 HotSpot VM 中使用。
![](https://engineering.linecorp.com/image/2018/8/2/1533208420073.png)
压缩具有如下优点:
- 避免碎片
- 依靠于压缩的分配,可以实现一个高性能的内存分配器(因为所有对象都位于堆的末尾,所以我们可以在右边,最后的位置,增加新的内存。)
![](https://engineering.linecorp.com/image/2018/8/2/1533208398145.png)
## 为什么 Go 不选择压缩的方式(Why Go did not opt for compaction)
来自 Google 的 Rick Hudson,在国际内存管理研讨会(ISMM)上,在他的 keynote 中分享到,[Getting To Go](https://blog.golang.org/ismmkeynote)。
- 在 2014 年,他们最初计划做一个任意读的并行拷贝 GC。
- 由于没有时间 - 彼时,他们正在将用 C 写的运行时改成用 Go 实现(对运行时的修改) - 他们决定选择 CMS。
- 他们采用了基于 TCMalloc 的内存分配器,解决碎片和优化(内存)分配的问题。
了解更多 Go 内存分配的内容,请看[运行时](https://golang.org/doc/go1.4#runtime) 的评论。
- 这最初基于 [tcmalloc](http://goog-perftools.sourceforge.net/doc/tcmalloc.html),但已经修改了很多的地方。- [malloc.go](https://github.com/golang/go/blob/release-branch.go1.10/src/runtime/malloc.go#L7)
## 分代的 GC(Generational GC)
分代 GC 的目的是通过将堆中对象除以它的年龄(他们从 GC 中存活的次数)来优化 GC,从而产生分代。分代假说指出,在许多应用中,新事物大多年轻。基于该假设,通过以下策略来 (优化)GC,也就是说,我们可以取消多次对旧对象的扫描。
- 从年轻的空间(Minor GC)中更频繁地收集垃圾。
- 可以将它们,在空间中已经存活了几个 GC 周期的旧对象,重新放置在不经常收集垃圾的空间(Major GC)中
Java8 HotSpot Vm 的所有收集器都实现了分代 GC。
## 写入屏障(Write barrier)
分代 GC 的缺点是,即使垃圾收集没有运行,对于应用程序也有开销。我们来看一个 Minor GC 的例子。
![](https://engineering.linecorp.com/image/2018/8/2/1533208408997.png)
如果我们仅检查 root 用于指向 Minor 的指针,然后收集无法访问的对象,那么旧对象中引用新对象(如图中的 obj2)会被意外地收集。但是,如果我们检查整个堆,包括旧对象以避免收集 Minor 对象(时产生的问题),那么对于分代 GC 来说就没有意义。因此,添加一个进程,以便在替换或重写引用时将旧对象的引用记录保存到新对象中。我们将此额外流程称为写入屏障。使用分代 GC 可能有更多好处,可以弥补这个缺点(写入屏障开销)。
## 为什么不使用分代 GC ?(Why not generational GC?)
正如我们之前看到的,分代垃圾收集器需要一个写屏障来记录代之间的指针。回到 Rick Hudson 的主题演讲,Getting To Go,我们可以看到他们确实考虑过分代 GC,但由于写屏障开销而放弃了它。
> 写屏障速度很快,但简单来说,它还不够快。
使用 Go,编译器的逃逸分析非常出色,如果需要,程序员可以控制到,不在堆上分配对象,因此短期对象通常分配在栈中而不是在堆中;这意味着不需要 GC。总的来说,你从分代 GC 得到的(好处)比其他(语言)运行时少。有一些用 Go 语言编写的库,跟速度一样出名的是,这些库恰好也是零内存分配。尽管如此,我们仍然有消耗,在每次 GC 循环中多次扫描长寿命的对象。来自 Google 的 Ian Lance Taylor 已经在 Golang-nuts 中提到了这一点,[为什么垃圾收集器不实现分代 GC 功能?](https://groups.google.com/forum/#!topic/golang-nuts/KJiyv2mV2pU)
- 这是个很好的问题。Go 当前的 GC 显然做了一些额外的工作,但它也跟其他的工作并行执行,所以在具有备用 CPU 的系统上,Go 正在作出合理的选择。请看 [https://golang.org/issue/17969](https://golang.org/issue/17969)
## 结束语(Closing notes)
通过研究 Go 垃圾收集器,我能够理解 Go GC 当前结构的背景以及它如何克服它的弱点。Go 发展得非常快。如果你对 Go 感兴趣,最好继续留意它(当我写这篇文章时,2018 年 8 月,Go 发布了它的 1.11 版本)。
via: https://engineering.linecorp.com/en/blog/detail/342/
作者:Haruki Okada 译者:gogeof 校对:polaris1119
本文由 GCTT 原创翻译,Go语言中文网 首发。也想加入译者行列,为开源做一些自己的贡献么?欢迎加入 GCTT!
翻译工作和译文发表仅用于学习和交流目的,翻译工作遵照 CC-BY-NC-SA 协议规定,如果我们的工作有侵犯到您的权益,请及时联系我们。
欢迎遵照 CC-BY-NC-SA 协议规定 转载,敬请在正文中标注并保留原文/译文链接和作者/译者等信息。
文章仅代表作者的知识和看法,如有不同观点,请楼下排队吐槽