自从Go1.5引入了真正的并发GC后, Go1.6进一步进行了优化,使得Go在上百G级的堆大小时依然能将STW时间控制在20ms以内:
而Java8的G1收集器,默认参数下在100G以上的heap下,会造成秒级的STW。虽然可以通过-XX:MaxGCPauseMillis
调整,但是是以牺牲大量吞吐量为代价。这里浅析一下Go能做到比G1更短的STW的原因。
轮流挂起协程
JVM的CMS收集器在工作时,大致分为4个阶段:
- 初始标记
- 并发标记
- 重新标记
- 并发清除
其中1, 3 是需要STW的阶段,CMS的停顿也是由这2个阶段引发的。Go1.5中的CMS也分为这些阶段,其中1, 3同样需要STW。那为什么Go会停顿时间更少呢?原因是,Go的CMS在第3阶段并不是挂起所有goroutine,而是轮流挂起。如此一来,3阶段就不会造成整个程序的停顿,从而就没有算入到STW时间之中。
Go触发GC的时机
Go的gc触发条件也与JVM的gc有很大区别。JVM通常是堆的使用到达某一阀值,或发生new
操作失败时gc。而Go则是当从上次gc以来,新创建的对象大小等于上次gc以后存活下来的对象时触发gc. 这样,每次gc的压力就不会像JVM那么大,STW时间理所当然会短很多,但也牺牲了吞吐量。
Go比Java产生更少的内存垃圾
Go的对象(即struct类型)是可以分配在栈上的。Go会在编译时做静态逃逸分析(Escape Analysis), 如果发现某个对象并没有逃出当前作用域,则会将对象分配在栈上而不是堆上,从而减轻了GC压力。其实JVM也有逃逸分析,但与Go不同的是Java无法在编译时做这项工作,分析是在运行时完成的,这样做一是会占用更多的CPU时间,二是不可能会把所有未逃逸的对象都优化到栈中。
到目前为止,Go的运行效率并没有因为执行的是本地机器码而体现出比Java更好的性能优势,我想这并不怪go, 而是JVM在JIT的帮助下其性能已经非常好了。虽然如此,Go的优化空间会比JVM更大。相信未来Go的性能会不断提升,终究会超过JVM。
有疑问加站长微信联系(非本文作者)