转载

Go GC

大家好,我是 Okada( @ocadaruma ),LINE 广告平台团队的成员。我对 Go 的 GC (垃圾收集器)有点感兴趣,甚至还促使我专门写一篇关于它的博客。Go 是一门由 Google 开发,并且支持垃圾收集的编程语言。Go 通过 管道 支持并发。很多的公司,包括 Google,都在使用 Go,LINE 也用 Go 来开发工具和服务。

Go GC

用 Go,你可以很容易地创建出低延时的应用。Go GC 似乎比其他语言的运行时要简单得多。对于 Go 1.10 版本,它的垃圾收集器是 Concurrent Mask & Sweep (CMS) ,它不是压缩的,也不是分代的。这一点跟 JVM 不同。

  • 它是一个,并行标记,用一个写屏障(写的时候阻塞)的清理(程序)。它是非分代,非压缩的。 -- 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 使用的收集器,就是非迁移的。一般来说,如果你在非迁移的垃圾收集器中,重复地进行内存分配跟释放,最终将导致堆碎片,从而降低分配(堆内存)的性能。但,当然,这也取决于你的内存分配器如何实现。

Go GC

迁移的 GC(moving GC)

移动垃圾收集器将活动对象移动到堆的末尾来压缩堆。移动垃圾收集器的一个实例是拷贝 GC(Copying GC),它在 HotSpot VM 中使用。

Go GC

压缩具有如下优点:

  • 避免碎片
  • 依靠于压缩的分配,可以实现一个高性能的内存分配器(因为所有对象都位于堆的末尾,所以我们可以在右边,最后的位置,增加新的内存。)

Go GC

为什么 Go 不选择压缩的方式(Why Go did not opt for compaction)

来自 Google 的 Rick Hudson,在国际内存管理研讨会(ISMM)上,在他的 keynote 中分享到, Getting To Go 。

  • 在 2014 年,他们最初计划做一个任意读的并行拷贝 GC。
  • 由于没有时间 - 彼时,他们正在将用 C 写的运行时改成用 Go 实现(对运行时的修改) - 他们决定选择 CMS。
  • 他们采用了基于 TCMalloc 的内存分配器,解决碎片和优化(内存)分配的问题。

了解更多 Go 内存分配的内容,请看 运行时 的评论。

  • 这最初基于 tcmalloc ,但已经修改了很多的地方。- malloc.go

分代的 GC(Generational GC)

分代 GC 的目的是通过将堆中对象除以它的年龄(他们从 GC 中存活的次数)来优化 GC,从而产生分代。分代假说指出,在许多应用中,新事物大多年轻。基于该假设,通过以下策略来 (优化)GC,也就是说,我们可以取消多次对旧对象的扫描。

  • 从年轻的空间(Minor GC)中更频繁地收集垃圾。
  • 可以将它们,在空间中已经存活了几个 GC 周期的旧对象,重新放置在不经常收集垃圾的空间(Major GC)中

Java8 HotSpot Vm 的所有收集器都实现了分代 GC。

写入屏障(Write barrier)

分代 GC 的缺点是,即使垃圾收集没有运行,对于应用程序也有开销。我们来看一个 Minor GC 的例子。

Go GC

如果我们仅检查 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 功能?

  • 这是个很好的问题。Go 当前的 GC 显然做了一些额外的工作,但它也跟其他的工作并行执行,所以在具有备用 CPU 的系统上,Go 正在作出合理的选择。请看 https://golang.org/issue/17969

结束语(Closing notes)

通过研究 Go 垃圾收集器,我能够理解 Go GC 当前结构的背景以及它如何克服它的弱点。Go 发展得非常快。如果你对 Go 感兴趣,最好继续留意它(当我写这篇文章时,2018 年 8 月,Go 发布了它的 1.11 版本)。

原文  https://studygolang.com/articles/16056
正文到此结束
Loading...