Go语言的实时GC原理和实践

来源:互联网 发布:mac触摸板怎么用 编辑:程序博客网 时间:2024/05/17 05:57

Go语言的实时GC原理和实践

每天,Pusher(原作者的一个程序)将数十亿的信息实时地(准确地说是从发送方到达接收方所需时间在100毫秒以下),其重要原因是Go语言的低延迟垃圾回收实现。

 
垃圾回收会导致程序的暂时停止,这是所有实时系统烦恼的根源之一。这篇博客将介绍Go语言的GC机制,从而理解它的高效之处。

 

从Haskell到Go

现在的Pusher原来是用Haskell语言写的,后来才用Go重写,根本原因在于GHC有根本性的延迟问题。更具体地说,是因为GHC的垃圾回收器是根据Working set(内存内的对象)到达某一个比例后来进行一次会导致全局中止的垃圾回收处理。这样当内存中存在大量对象时,会每隔数百毫秒就进行一次垃圾回收,而且是没有意义的垃圾回收(可能只有极少数的未被引用的对象)。

 
而Go的垃圾回收期则可以与其他线程并行执行,避免了全局停止时间的出现。甚至每一次版本更新,都能感受到延迟的进一步降低,可见Go开发团队对此的重视程度。

 

并行垃圾回收的运行原理

到底Go是如何实现GC的并行处理的呢?其核心在于三色标记和扫除算法,一下的图片将展示此算法的运行机制,请重点留意它是如何实现并行处理的。

 

时期1:程序运行

时期1:程序运行

改程序对多个链表进行操作,一开始共有A、B、C三个节点对象,红色的对象A和B是根对象,通常来说都是可达的。垃圾回收器将对象分为黑、灰、白三个集合,因为现在GC周期还没开始,所以他们都属于白色集合。

 

时期2:程序运行

时期2:程序运行

新增一个对象D,作为A的next节点,因为一般GC线程初始化之后新增的对象都会被分到灰色集合中,D也不例外。

 

时期3:GC扫描

时期3:GC扫描

GC周期开始时,根对象都将移动到灰色集合中,此时灰色集合中有A、B、D三个对象。注意,此时正常程序并没有因此而停止。

 

时期4:GC扫描

时期4:GC扫描

为了实行扫描,GC首先选择根对象A,把它移动到黑色集合并把它所引用的子对象移动到灰色集合,此时A只引用了D,而D本来就属于灰色集合,因此并不需要移动。无论进行到哪个阶段,GC都可以计算出剩余的对象移动次数=2*|white|+|grey|,把全部阶段都完成至少需要一次的移动,以使得剩余的对象移动次数=0。

 

时期5:程序运行

时期5:程序运行

新对象E生成,并作为C的next节点,正如时期2所说的,它被分配到灰色集合。程序也因此而增加了所需的GC阶段数,导致最终的扫除阶段被延迟。

 

时期6:程序运行

时期6:程序运行

此时B把next指针指向了对象E,从而使对象C变成不可达对象。也就是说,对象C将残留在白色集合中,这个集合正是最终的扫除阶段会被回收内存空间的。

 

时期7:GC扫描

时期7:GC扫描

扫描继续进行,GC这次选择了对象D,但D并没有下层对象,即本次无可移动到灰色集合的对象。

 

时期8:程序运行

时期8:程序运行

此时B又把next指针设置为空,对象E也变得不可达。嗯,正如你所想的,E位于灰色集合中,并不能被回收,是不是会有内存泄露的风险啊?但这实际上并不是问题,E将在下一次GC周期被回收。三色标记和扫除算法能保证在GC周期开始时不可达的对象将会在周期结束时被回收。

 

时期9:GC扫描

时期9:GC扫描

GC这次选择了对象E进行扫描,因此E将被移动到黑色集合,但E并没有下层对象,注意这里对象C将永远不会移动到其他集合,因为他是E的上层对象而不是下层对象。

 

时期10:GC扫描

时期10:GC扫描

GC在最后将选择灰色集合中的对象B进行扫描,此时灰色集合将变为空。

 

时期11:GC清除

时期11:GC清除

GC将回收白色集合中的对象(垃圾)的内存空间,它们是绝对的不可达对象,可以放心地杀死。而对象E是在该GC周期内突然变得不可达的,将留在下一个GC周期才被清除。

 

时期12:GC重置

时期12:GC重置

在实际运用中,没有必要把所有黑色集合中的存留对象再移动会白色集合,只需将黑色集合重新解释为白色集合,白色集合重新解释为黑色集合即可,既简单又快速。

 

两个全局停止时期

第一个是为了确定根对象的栈空间扫描,第二个是GC扫描的最终清除阶段。好消息是,第二个全局停止时期在最近的版本中已经被优化了,完全可以避免。然而这两个全局停止时期即使对于一个很大的堆也不用1毫秒即可完成。

 

Latency(延迟) vs. Throughput(吞吐量)

即使利用并行GC对于很大的堆也能提供大规模的低延迟服务,那为什么还有很多人选择全局停止的垃圾回收器(比如Haskell的GHC)呢?Go的并行垃圾回收器与GHC的全局停止GC估计只好那么一点点吧?

 
实际上,Go实现如此的低延迟是有代价的,其中最大的是吞吐量的下降。由于需要实现并行处理,线程间同步和多余的数据生成复制都会占用实际逻辑业务代码运行的时间。GHC的全局停止GC对于实现高吞吐量来说是十分合适的,而Go则更擅长与低延迟。

 
并行GC的第二个代价是不可预测的堆空间扩大。程序在GC的运行期间仍能不断分配任意大小的堆空间,因此我们需要在到达最大的堆空间之前实行一次GC,但是过早实行GC会造成不必要的GC扫描,这也是需要衡量利弊的。因此在使用Go时,需要自行保证程序有足够的内存空间。

0 0
原创粉丝点击