随着 2023 年秋季 JDK 21 的发布,现在有一个新的 LTS 版本来基准测试和生成一些 GC 性能图表。 JDK 21 和 JDK 17 之后的其他版本提供了一系列值得注意的功能,例如虚拟线程、Switch 模式匹配和分代 ZGC。 让我们看看它的表现如何。
介绍
在比较不同 JDK 版本之间的性能时,可能很难确定哪些功能提供了一定的性能提升。 但很容易看出,自 JDK 8 以来,整个 J**a 平台的性能有了显著的提高。 在本文中,我将使用 specjbb 20151 来展示性能改进。 这是一个众所周知的标准基准,非常适合演示对 GC 的改进。 这主要是因为基准测试提供了两个分数:
max-jops:原始吞吐量critical-jops:延迟约束下吞吐量GC的提高会提高两个分数,但延迟约束下分数的提高与GC的变化关系更密切。 基本上,较短的超时将获得更好的分数。 对 JIT 和 J**a 平台其他部分的改进也会在原始吞吐量分数中发挥作用。
在基准测试时,我没有做太多调整,但我设置了 16 GB 的固定堆大小,并启用了大页面,并确保在运行基准测试之前对它们进行分页。 我希望结果能够反映即插即用的行为,但这样的配置会给你带来公平和一致的结果。
选择 GC
Oracle 支持 4 种不同的 GC,它们都有不同的用途。 在这篇文章中,我不包括串行 GC,因为它不适用于我正在使用的基准测试。 串行 GC 的主要重点是低开销,主要适用于内存和 CPU 资源有限的用例。 在这次比较中,我们将重点关注以下 GC:
g1:自 JDK 9 以来的默认收集器,重点关注延迟和吞吐量之间的平衡parallel:以吞吐量为导向的收集器,可能会遇到较长的最坏情况延迟z:超低延迟、完全并发、低于毫秒级的暂停时间的替代方案,以及使用哪种 GC 取决于应用程序最关心的问题。 每个 GC 都有最佳替代方案,并且没有一个 GC 在所有用例中都表现最佳。
进展
如果你回顾一下自 JDK 8 以来所取得的进步,就会发现 G1 和 Parallel 在各个方面都取得了惊人的进步。 两个收集器在各个方面都得到了改进。 它们暂停的时间更少,使用的内存更少,并且具有比以往更好的吞吐量。 ZGC 并没有那么久,在这篇文章中,我主要关注使 ZGC 成为世代收集器所带来的改进。
本文中的图表分别比较了不同的收集器。 主要原因是,根据堆大小的配置,结果对某个收集器更有利。 通过这样做,我们可以专注于所有 GC 的巨大进步,而不是试图提供最好的 GC。
比较包括 JDK 8、JDK 17 和 JDK 21 的 G1 和 Parallel。 对于 ZGC,我选择的三个数据点是 JDK 17、JDK 21 和 JDK 21 中的分代 ZGC。 由于 JDK 17 是第一个完全受 ZGC 支持的 LTS 版本,因此回顾早期版本没有多大意义。
吞吐量
在原始吞吐量性能方面,自 JDK 17 以来的改进并不大,但仍有轻微的提升。 但是,在下面的图表中,有两件事确实值得关注。 首先,JDK 8 和最新的 JDK 之间的显着区别在于 G1 和 Parallel。 从性能的角度来看,离开 JDK 8 从未像现在这样有益。
第二个值得注意的注意事项是使用分代 ZGC 时看到的 10% 提升。 ZGC 中的新一代支持使其在内存中更加高效,而不必考虑整个堆的每个 GC。 其效果是,在执行 GC 工作时消耗的 CPU 资源更少,可用于应用程序,从而提高其性能。
延迟
对于延迟评分,情况基本相同。 G1 和 Parallel 在 JDK 8 和 JDK 17 之间取得了巨大的进步,但最好的结果仍然是在 JDK 21 中。 值得注意的是,JDK 17 和 JDK 7 之间有 8 年多的创新时间,而 JDK 17 和 JDK 21 之间只有两年的创新时间。 相比之下,指导性案例的短时间框架和成熟度使得在如此大的基准上取得了重大进展。
将几代人引入 ZGC 并仍然看到世代 ZGC 与传统模型之间的显着差异,这真是太好了。 值得注意的是,大部分改进来自吞吐量分数的增加。 这两种 ZGC 模式之间的暂停时间没有太大差异,两者都远低于 1 毫秒。 但是,当考虑到最坏情况的延迟时,与传统模式相比,分代 ZGC 要好一些。
对于 G1 和 Parallel,暂停时间没有太大变化。 我们在 g1 上花了更多的时间,在这里,通过查看更高的百分位数停顿,我们可以看到我们已经设法将时间缩短了几毫秒。
内存开销
最后一个图表比较了在固定负载下运行基准测试时的峰值本机内存开销。 从这个角度来看,Parallel 非常稳定,我们没有花任何时间进一步优化它。 另一方面,在G1中,我们在过去十年中设法消除了许多低效率的问题,在JDK 20中,我们将G1更改为只需要一个标记位图,而不是两个。 由此节省的资源非常显著,G1 现在是该基准测试中内存效率最高的收集器。
对于分代 ZGC,我们可以清楚地看到为了在此基准测试中获得更好的延迟和吞吐量而做出的权衡。 权衡是更高的本机内存消耗。 为了有效地实现代际支持,我们需要跟踪从老一代到新一代的指针。 这些称为内存集,它们占用内存。 当处理多代时,我们还需要一些内存来存储其他元数据。 尽管如此,在大多数情况下,与传统 ZGC 相比,使用分代 ZGC 时,总体内存消耗会更低,因为它不需要处理给定工作负载所需的堆大小。 因此,通常可以通过使用较小的堆来节省额外的本机内存使用量,并且仍然可以获得更好的整体性能。
升级并尝试分离 ZGC 的代次
如上所述,与 JDK 8 相比,JDK 21 的性能得到了显著提高。 因此,如果您仍在使用 JDK 8,您应该开始考虑升级。 这也是在升级时重新评估要同时使用的 GC 的好时机。 如果迁移到 JDK 21,我强烈建议尝试分代 zgc。 在 JDK 21 中,ZGC 在分代版本和旧版本中都可用,要使用分代版本,您需要指定以下两个选项:
-xx:+usezgc -xx:+zgenerational
**: Stefan Johansson, link: Kstefanjgithub.io/2023/12/13/jdk-21-the-gcs-keep-getting-better.html