【译】JDK 21:GC变得越来越好了!

JAVA架构日记

共 3176字,需浏览 7分钟

 · 2024-04-10

几年前,我写了一篇关于JDK 8和JDK 17之间我们的三个主要垃圾收集器进展的文章《JDK 17 G1/Parallel GC 改进【译】》。随着今年秋季发布的JDK 21版本,现在有了一个新的LTS版本来进行基准测试,并生成一些垃圾收集性能图表。自JDK 17以来的JDK 21和其他版本都提供了一系列值得关注的功能,如虚拟线程switch语句的模式匹配分代ZGC。让我们看看它的表现如何。

一、简介

当比较 JDK 版本之间的性能时,很难准确地说出哪些功能带来了某种性能提升。但是很容易看出自 JDK 8 以来,Java 平台整体的性能显著提高。在本文中,我将使用 SPECjbb®2015 来展示性能增益。这是一个众所周知的标准基准测试,非常适合展示 GC 的变化。主要原因在于该基准测试提供了两个分数:

  • max-jOPS - 原始吞吐量
  • critical-jOPS - 在延迟约束下的吞吐量

GC 的改进将提高这两个得分,但限制延迟得分的改进更与 GC 的变化相关。基本上,较短的暂停将给出更好的得分。对于原始吞吐量得分,JIT 和 Java 平台的其他部分的改进也起到作用。我没有进行太多调整就运行了基准测试,但是我设置了一个固定的堆大小为 16GB,启用了 large pages 并确保在运行基准测试之前将其分页。我希望结果能反映出开箱即用的行为,但像这样的配置可以获得公正和一致的结果。

二、选择GC

Oracle支持4种不同的垃圾收集器,它们都适用于不同的用例。在本文中,我不包括 Serial GC,因为它不适用于我使用的基准测试。Serial GC 的主要关注点是低开销,主要适用于内存和CPU资源有限的用例。在这次比较中涵盖的垃圾收集器有:

  • G1 GC - 自JDK 9起,默认的GC,注重延迟和吞吐量之间的平衡。
  • Parallel GC - 以吞吐量为导向的GC,可能会遇到较长的延迟。
  • ZGC - 这是一种超低延迟的选择,完全并发且具有亚毫秒级的暂停。

选择使用哪种 GC 取决于你的应用程序最重要的是什么。每种GC都有适用的用例,没有一种 GC 能够在所有情况下都达到最佳效果。这篇《JDK 17 G1/Parallel GC 改进【译】》提供了更多详细信息。

三、进展

如果我们回顾到JDK 8,对于G1和Parallel收集器所做的改进是相当非凡的。这两个收集器在各个方面都有所提升。它们具有更短的暂停时间,使用更少的内存,并且具有比以往更好的吞吐量。ZGC还没有参与得太久,在这篇文章中,我主要关注的是将ZGC改造为分代ZGC带来的改进。这篇帖子中的图表将各个垃圾收集器进行了比较。这样做的主要原因是,根据堆大小的配置,结果对某个垃圾收集器更有利。通过这样做,我们可以专注于所有GC 取得的巨大进展,而不是试图评选出最佳的垃圾收集器。这些比较包括 JDK 8JDK 17JDK 21 的 G1 和 Parallel 收集器。对于 ZGC,我选择了 JDK 17、JDK 21 和 JDK 21 中的分代 ZGC 这三个数据点。由于 JDK 17 是第一个完全支持 ZGC 的 LTS 版本,所以进一步回顾之前的版本并没有太多意义。

3.1 吞吐量

就原始吞吐量性能而言,自JDK 17以来的增益并不是很大,但仍然有轻微增加。但在下面的图表中,有两个真正需要关注的事情。首先,G1和Parallel在JDK 8和最新的JDK之间存在显著差异。从性能角度来看,抛弃JDK 8升级到新版JDK从未像现在这样有益。

a33962e0debfce9292c9f70f21c1d1c9.webp

需要强调的第二点是使用分代ZGC时所见到的10%的改进。ZGC中的新一代支持使其能够更高效地回收内存,无需在每次垃圾收集时考虑整个堆。效果是,在执行垃圾收集工作时消耗的CPU资源较少,这些资源可以被应用程序使用,从而提高性能。

3.2 延迟

就延迟得分而言,情况大致相同。G1和Parallel在JDK 8和JDK 17之间取得了很大的进展,但最好的结果仍然是在JDK 21中。我们应该记住,在JDK 8和JDK 17之间进行了超过7年的创新,而在JDK 17和21之间只有两年时间。较短的时间跨度以及垃圾收集器现在运行得相当良好的事实,使得在像这样的大型基准测试中难以取得巨大的进展。

76201861fa9f02cd00943085ffda41cd.webp

将分代添加到ZGC中仍然使得在分代ZGC和遗留模式之间看到显着变化成为可能,这非常好。应该注意的是,大部分增益来自于吞吐量得分的改进。两种ZGC模式之间的暂停时间长度并没有太大差别,它们都远低于1毫秒。不过,当考虑最坏情况的延迟时,分代ZGC与ZGC相比又要稍微好一点。

4f95ab158d7e2c6a5b160aad3163c81b.webp

就G1和Parallel而言,在暂停时间方面并没有太大的改变。我们在G1和Parallel上花费更多的时间,并且在查看高百分位数的暂停时间时,我们可以看到我们已经能够减少几毫秒。

3.3 内存占用

最后一张图比较了在固定负载下运行基准测试时的峰值本机内存开销。从这个角度来看,Parallel非常稳定,我们没有花费任何时间尝试进一步优化它。另一方面,对于G1,过去十年中我们已经能够消除许多低效率,而在JDK 20中,我们将G1更改为只需要一个标记位图而不是两个。由此产生的节省是显著的,在这个基准测试中,G1现在是最节省内存的收集器。

1d19d724a5c126916872cfdd2c36449d.webp

对于分代ZGC,我们可以清楚地看到在这个基准测试中为了获得更好的延迟和吞吐量所做的权衡。代价是更高的本机内存消耗。为了有效实现代支持,我们需要跟踪从老年代到年轻代的指针。这称为记忆集,它们会消耗内存。处理多个代时,我们还需要一些其他元数据所需的内存。话虽如此,在大多数情况下,与遗留ZGC相比,使用 分代ZGC的总内存消耗 更低,因为它不需要太多的堆来处理给定的工作负载。因此,通过使用较小的堆仍然可以获得更好的整体性能,从而节省额外的本机内存使用。

四、升级和尝试分代ZGC

正如你现在所看到的,JDK 21相比JDK 8在性能方面有了显著的提升。因此,如果你仍在使用JDK 8,你应该考虑升级。在升级时,重新评估使用哪种垃圾收集器也是一个很好的机会。如果迁移到JDK 21,我强烈建议尝试一下分代ZGC(Generational ZGC)。在JDK 21中,既提供了分代版本的ZGC,也提供了传统模式ZGC,如果要使用分代ZGC,你需要同时指定这两个标志:

      
      -XX:+UseZGC 
-XX:+ZGenerational

我们最终的目标是摆脱传统模式,并且为了顺利实现这一目标,我们希望用户能就分代ZGC在某些用例中性能不如传统ZGC的情况提供反馈意见。

五、译者说

大家好我是如梦技术——春哥!由于精力有限,很多好文躺在我的待翻译列表里,希望此篇译文也对大家有所帮助!现在有不少的朋友在升级 Spring boot 3.2.x 和 JDK 21。对于普通服务来说无脑G1 GC就够了,也不用去背那些八股文。对于数据量大、内存占用大的场景,例如 kafka、rocketmq 等可以考虑ZGC或分代ZGC来减少垃圾回收引起的停顿时间。

8e45d8f059f4f5a03feca10c8516e88d.webp

六、相关译文

浏览 11
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报