Go的内存垃圾回收性能
在Go语言简介一文中我们对Go语言的语法和基本特性做了一个简单介绍。Go的一个重要能力是能够对内存垃圾进行回收。 这是传统编程语言比如C/C++等没有的。想当初写C/C++程序时,内存泄漏和非法内存访问是程序员最为头疼的问题。程序复杂了,项目团队大了,内存泄漏和非法内存访问便成了司空见惯的现象。通过内存垃圾收集,Go基本没有这个问题了,这对降低编程复杂度,提高程序的健壮性有很大的帮助。
Go的内存垃圾回收是一个根据Dijkstra等人在1978年提出的一个算法实现的。这是一个三色标示-扫除算法。这个算法的基本工作方式是将垃圾回收分为两个阶段,第一阶段是标示阶段,在这个阶段中,回收器会将内存资源按一定规则分为三种颜色:黑色,灰色,和白色。在扫除阶段,回收器会把被标示成白色的内存资源当垃圾回收,因为白色的是没有任何程序使用的内存。这样,内存就不存在泄漏这个问题了。
为避免在扫除过程中其它程序对内存进行修改而造成的竞争条件,在扫除中,需要将所有程序停下来,这个动作被称做“停止整个世界(STW)”。很显然,STW的时间越长,整个系统的效率就越低,因为正事干不了了,都在做垃圾清扫。所以,STW时间长短是评估内存垃圾回收性能的关键指标。下面我们就以这个指标为主题讨论Go这一路走来在内存垃圾回收这个方面的进步。
Go1.2版之前的垃圾回收暂停时间会让应用程序停滞十几秒甚至更长【1】。这对交互要求高的应用程序来说几乎是不可接受的。Go1.2版将这个时间降低到了数秒。1.3版是1秒左右,1.4版300毫秒。到Go1.5,由于采用了并发和增量算法,大部分垃圾收集工作不需要将应用停下来,所以暂停时间缩短到40毫秒的范围,进步神速。
下图给出了Go1.3,1.4,1.5版之间的暂停时间的差异。有一点要特别说明的是,Go1.5版对不同的堆空间的大小基本保持远低于1秒的的暂停时间,而更早期的版本随着堆空间的增大,暂停时间迅速增加,比如,对25GB的堆空间,Go1.4的平均暂停时间增加到4秒左右。
图1 Go1.3,1.4,1.5内存垃圾回收暂停时间
虽然Go1.5的暂停时间在毫秒级,但有个缺陷:在堆空间比较大的情况下,它的暂停时间迅速增加,如图2所示,在25GB的堆空间,Go1.5的暂停时间增加到55毫秒以上。考虑到大型应用程序所需内存远远大于25GB,那Go1.5的暂停时间就不可接受了。Go1.6基本克服了这个缺陷,即便堆空间增加到250GB,Go1.6基本保持了10毫秒的暂停时间,个别情况下这个时间接近40毫秒。就这个来说,Go1.6基本能满足大多数应用的要求。
图2 Go1.5和1.6版内存垃圾回收暂停时间比较
到Go1.6.3,这个暂停时间降到了3毫秒左右了,如图3所示:
图3 Go1.6.x内存垃圾回收暂停时间比较
Go1.7的垃圾回收暂停时间基本稳定在3毫秒左右。到Go1.8,通过进一步优化算法,不再做栈的重扫,暂停时间降到了100微秒,很多时候,可以做到10微秒,如图4所示:
图4 Go1.8内存垃圾回收暂停时间
从2007年提出到现在,Go语言已经十岁了。十年磨一剑,Go语言的成长从它的内存垃圾回收器的性能的提升可见一斑。在此对Go语言团队的持续努力表示敬意,也希望Go语言在应用开发中越来越成功。
【参考文献】
- https://blog.twitch.tv/gos-march-to-low-latency-gc-a6fa96f06eb7
- https://talks.golang.org/2015/go-gc.pdf
- https://talks.golang.org/2016/state-of-go.slide#1
- https://twitter.com/brianhatfield/status/804355831080751104
- https://github.com/golang/proposal/blob/master/design/17503-eliminate-rescan.md
- https://making.pusher.com/golangs-real-time-gc-in-theory-and-practice/