go GC: prioritizing low latency and simplicity

Let’s Summarize #

介绍了当前软硬件大规模发展的趋势以及go GC需要优先解决的问题:低延迟和简单性(通过一个参数就可以控制,而非像JVM调参那样)。

go团队的目标是设计一个面向未来十年的垃圾回收器,借鉴了十几年前发明的算法。go GC使用的是并发三色标记清除算法(concurrent, tri-color, mark-sweep collector),由Dijkstra在1978年提出。该算法与现在大多数企业级的GC实现不同,但是go团队认为该算法更适合于现代硬件的发展,也更有助于实现现代软件的GC低延迟目标。

该GC算法中,每个对象只能是white、grey、black中的其中一种,heap可以看做是互相连接的对象构成的一个graph。GC算法流程是:

  • GC开始时,所有对象都是white;
  • GC遍历所有的roots对象(比如全局变量、栈变量)将其标记为灰色;
  • 然后GC选择一个grey对象,将其标记为black,并扫描(scan)该对象检查它内部的指向其他对象的指针。如果发现有指针指向其他white对象,将white对象标记为grey;
  • 该过程重复执行,直到没有任何的灰色对象;
  • 最后,剩下的白色对象即认为是不可达对象,可以被回收再利用;

GC过程和应用程序执行是并发进行的,应用程序也称为mutator,它会在GC运行期间修改一些指针的值。mutator必须遵循这样一条规则,就是不允许出现一个黑色对象指向一个白色对象,这样会导致对象被错误地回收。为了保证该规则成立,就需要引入写屏障(write barrier),它是编译阶段由编译器对mutator指针操作安插的一些特殊指令,用来跟踪对指针的修改,write barrier如果发现当前黑色对象的内部指针字段指向了外部的一个白色对象,则会将白色对象染色为grey,避免其被错误地GC掉,也保证其可以被继续扫描。

有些GC相关的问题:

  • 什么时候启动GC?
  • 通过哪些指标来判断要启动GC?
  • GC应该如何与scheduler进行交互?
  • 如何暂停一个mutator线程足够长时间,以扫描器stack?
  • 如何表示white、grey和black三种颜色来实现高效地查找、扫描grey对象?
  • 如何知道roots对象在哪里?
  • 如何知道一个指向对象的指针的位置?
  • 如何最小化内存碎片?
  • 如何解决cache性能问题?
  • heap应该设置为多大?
  • 等等。

上述问题有些与内存分配有关,有些与可达对象分析有关,有些与goroutine调度有关,有些与性能有关,关于这些内容的讨论远远超出本文篇幅,可以自己参考相关的材料。

为了解决GC性能问题,可以考虑为每一种优化加个参数来控制,开发人员可以自己调整这里的参数来达到想要的优化效果。但是这种做法时间久了之后会发现有非常多的参数,调优就会变得非常困难,比如JVM调优。go团队不想走这样的老路,力求简单高效。

go通过GOGC这个环境变量来控制整个堆大小相对于现阶段可达对象大小的比例。GOGC默认值是100%,意味着当堆大小增长了当前可达对象大小的1倍时(2倍大小),就会触发GC;200%则意味着继续增长了当前可达对象的2倍时触发GC(3倍大小)。

  • 如果想降低GC花费的时间,就把这个值设置的大一点,因为这样不容易频繁触发GC;
  • 如果愿意花费更多的GC时间来换取更少的内存占用,就把这个值设置的小一点,因为这样能够更加频繁地GC;

前面提到go团队要设计一个面向未来十年的垃圾回收器,未来十年机器内存容量可能会翻倍或者成倍增长,简单地将GOGC设置为一定倍率也可以很好地工作,也不用像JVM调优那样重新设置一堆地参数,调参大军好惨。go团队也可以倾听用户真正地诉求在运行时方面做更多的优化。

Source Analysis #

References #

  1. https://blog.golang.org/go15gc