我们都知道 OpenJDK8 有好几个垃圾回收算法,比如 ParallelGC,CMS,还有 G1,那么哪个才是最快的?如果 GC 算法从 Java8 中默认的 ParallelGC 切换到 G1 会发生什么(JDK9 就是把默认 GC 从 ParallelGC 切到了 G1)?废话不多说,做一个基准测试就知道了,Let’s benchmark it.
分别用不同的 JVM 参数运行 6 次同样的代码。这些VM参数为:-XX:+UseSerialGC, -XX:+UseParallelGC, -XX:+UseConcMarkSweepGC, -XX:ParallelCMSThreads=2, -XX:ParallelCMSThreads=4, -XX:+UseG1GC。
每次运行大概花 55 分钟。
除了指定 GC 的 JVM 参数,其他的 JVM 参数为:
每次通过 optaplanner 解决 13 个问题,每个问题大概 5 分钟,并且前 30 秒的 JVM 预热时间不计算在内。
解决问题时不会发生IO,运行过程中,单个CPU完全饱和,并且会一直创建很多生命周期很短的对象,然后GC负责收集它们。
基准测试测量每毫秒能被计算的分数,越高表示越好。需要说明的是,计算一个分数可不是一件容易的事情,它涉及很多计算,有兴趣的话,可以去 optaplanner 查看它们的源码。
如果想要在你的本地复制演示这个基准测试,只需要基于源码构建 optaplanner,然后运行 main 类 GeneralOptaPlannerBenchmarkApp
即可。
OptaPlanner: https://www.optaplanner.org 。
每个垃圾回收器和 Java8 默认的 ParallelGC 的对比如下图所示:
结果非常清晰,JDK8 默认的 ParallelGC 是最快的,其他垃圾回收器相比默认的 ParallelGC 都会有不同程度的衰减,并且 G1 表现最差,是最慢的。
JDK 有一个提案建议将 G1 作为服务端默认的垃圾收集器,详情请戳链接:JEP 248: Make G1 the Default Garbage Collector: http://openjdk.java.net/jeps/248 ,我的第一反应是拒绝这个提案,并且上面的压测结果给了我充足的理由:
其他方面,也有一些需要注意的地方:
自 G1 完全支持以来,它一直被吹捧为 CMS 的替代者。但是,社区关心的这个JEP248 却是要用 G1 取代 ParallelGC,而不是 CMS。人们普遍认为,由于业务从 CMS 迁移到 G1,因此有数据对比 CMS 和 G1,但是却没有足够的数据对比 ParallelGC(现在默认的 GC)和 G1(准备默认的 GC)。同时,数据表明,许多业务还在使用默认的 GC,即 ParallelGC。因此,当 G1 变成默认的 GC 后,肯定能观察到一些 GC 行为的变化。
Java9 将 G1 设置为默认 GC 的最主要动机是 G1 能 减少 FullGC 的次数 ,这是 G1 相比 JDK8 默认的 ParallelGC 一个很大的改进。G1 的目标是在 不受到堆大小或存活对象数量限制 的情况下最小化暂停时间。这是通过并发进行大部分 GC 工作,只对部分堆的压缩来实现的,这个 GC 过程被称为 mixed gc 。尽可能避免做 FullGC 是 G1 的主要优点之一。
通常来说,限制 GC 停顿时间比追求大吞吐量更重要。对许多用户来说,切换到 G1 这种低延迟的垃圾收集器相比 JDK8 以前默认的吞吐量优先的垃圾收集器 ParallelGC,应该能提供更好的整体体验。
G1 GC 被当做是 CMS 的长期替代品,现在的 CMS 有一个很大的问题,将导致并发模式失败,最终导致收集并压缩整个堆(FullGC)。当然,你也可以调优 CMS,延迟这种单线程压缩整个堆的 FullGC,但是,随着使用 CMS 的 JVM 运行的时间越来越长,最终一定 不能避免 (发生 FullGC),注意措词,是 一定 。
将来,这种 FullGC 会被优化成多线程并行(JDK10),但是,还是不能避免FullGC(只不过,现在是单线程 FullGC,以后是多线程 FullGC)感兴趣的话,请戳链接 JEP 307: Parallel Full GC for G1: https://openjdk.java.net/jeps/307。
另外一个重要的点是,即使是经验丰富的 GC 工程师,维护好 CMS 也被证明是非常具有挑战性和不确定性的。
还有,CMS,ParallelGC 以及 G1 都是基于不同的 GC 框架来实现的,如此以来,导致维护代价非常大。而 G1 是基于 Region 设计的堆框架,这是未来发展的方向。IBM 的 Balanced GC,Azul 的C4,以及 OpenJDK 的 Shenandoah GC,都是同类的基于 Region 设计实现。
ParallelGC 不能做递增式的收集。因此,它为了吞吐量就会牺牲延迟性。 随着负载加大,以及更大的堆,GC 的停顿时间也会增加 。这样的话,可能会影响与延迟相关的系统级协议(SLA)。
G1 能帮助你满足 SLA,而且 G1 的 Mixed GC 停顿时间远比 ParallelGC 的 FullGC 要短的多(当然,G1 也有 FullGC,但是其发生的次数可比 ParallelGC 的 FullGC 次数少很多)。
当前情况下,CMS 由于 碎片化问题 和 并发模式失败 问题而很可能无法满足 SLA,而调优后的 G1 却能够满足 SLA。G1 的mixed gc 最糟糕情况的停顿时间,也要比 CMS 遭遇的最糟糕的整个堆压缩停堆时间要更好。还有前面提到的,CMS 堆的碎片化问题,导致单线程的 FullGC 只能推迟, 不可能 完全阻止。
因此,CMS 已经被放弃,并且在 JDK9 中被标记为 deprecated,未来的版本会被移除,参考 JEP 291: Deprecate the Concurrent Mark Sweep (CMS) Garbage Collector,相关连接: http://openjdk.java.net/jeps/291
另外,像谷歌这样的公司,基于 OpenJDK 源码构建和运行它们自己私有的 JDK,其特定的源码根据它们的需求而有所改变。例如,谷歌工程师提到:为了减少碎片化,他们为他们的 CMS 的重新标记阶段,增加了一种增量压缩,使它们的 CMS 更可靠。
(参考链接: http://mail.openjdk.java.net/pipermail/hotspot-dev/2015-July/019534.html ).
注意:增量压缩也有自己的成本,谷歌可能在权衡其特定使用场景的好处后,才会增加增量压缩。
没有最好的垃圾回收器,只有更适合业务的垃圾回收器。如果对 GC 的停顿时间很敏感,那么请使用 G1,比如 WEB 服务器;如果对吞吐量有很大的要求,建议使用 ParallelGC,比如 OptaPlanner 这种测试用例。