3年前 (2021-09-06)  Java系列 |   抢沙发  437 
文章评分 0 次,平均分 0.0

ZGCShenandoah和对G1的改进使开发人员比以往任何时候都更接近无停顿Java。

在过去的六个月里,JDK的垃圾收集器(GC)出现了一些最令人兴奋的发展。本文介绍了一系列不同的改进,其中许多最早出现在JDK 12中,并在JDK 13中继续介绍。首先,我们将介绍Shenandoah,一种低延迟GC,它主要与应用程序同时运行。我们还将介绍作为JDK 12的一部分发布的ZGC(Java11中引入的低延迟并发GC)的最新改进。我们将详细解释垃圾优先Garbage First (G1)GC的两个改进,它是Java9以后的默认GC。

垃圾回收概述

java与C++等C++语言相比,Java最大的生产力优势之一就是使用垃圾回收。作为Java开发人员,如果没有显式释放内存位置,您基本上不需要担心内存泄漏,如果在使用完内存之前释放内存,您也不需要担心应用程序崩溃。垃圾收集是一个巨大的生产力胜利,但开发人员一次又一次地关注它的性能影响。它会减慢你的应用程序吗?它是否会导致应用程序个别暂停,从而导致用户体验不佳?

许多垃圾收集算法经过多年的尝试和测试,不断提高其性能。这类算法有两个共同的性能领域。第一个是垃圾收集吞吐量:应用程序的CPU时间中有多少用于执行垃圾收集工作而不是运行应用程序代码?第二个是创建的延迟,即单个暂停的延迟。

对于许多暂停GC(例如,并行GC,它是java9之前的默认GC),增加应用程序的堆大小可以提高吞吐量,但会延长最坏情况下的暂停时间。对于具有此配置文件的GCs,较大的堆意味着您的垃圾收集周期运行的频率较低,因此,更有效地分摊其收集工作,但单个暂停时间较长,因为单个周期中有更多的工作要做。在大型堆上使用并行GC可能会导致显著的暂停,因为收集旧一代已分配对象所需的时间会随着生成的大小以及堆的大小而增加。但是,如果您运行的是非交互式批处理作业,那么并行GC可能是一个高效的收集器。

自Java9以来,G1收集器一直是OpenJDK和Oracle JDK中的默认GC。G1的垃圾收集总体方法是根据用户提供的时间目标对GC暂停进行分段。这意味着,如果您想要较短的暂停时间,请设置一个较低的目标;如果您想要GC使用较少的CPU,而应用程序使用较多的CPU,请设置一个较大的目标。虽然并行GC是一个面向吞吐量的收集器,但G1试图成为一个万事通:它提供较小的吞吐量,但更好的暂停时间。

然而,G1并不擅长暂停时间。随着垃圾收集周期中要完成的工作量的增加,无论是由于堆非常大还是由于快速分配大量对象,时间切片方法开始遇到困难。打个比方,把一大块食物切成小块会使这些食物更容易消化,但如果你盘子里的食物太多,你就得花很长时间才能吃晚饭。垃圾收集的工作原理也是一样的。

这就是JDK 12的Shenandoah GC攻击的问题空间:它是延迟专家。它可以始终实现较低的暂停时间,即使在较大的堆上也是如此。与并行GC相比,它可能会花费更多的CPU时间来执行垃圾收集工作,但暂停时间会大大减少。这对于金融、赌博或广告行业的低延迟系统,甚至对于用户可能会因长时间暂停而感到沮丧的交互式网站来说,都是非常好的选择。

在本文中,我们将介绍这些GCs的最新版本以及G1的最新更新,并希望能帮助您找到最适合您的应用程序的功能平衡。

Shenandoah

Shenandoah是作为JDK 12的一部分发布的一个新GC。事实上,Shenandoah开发工作也支持对JDK 8u和11u版本的改进,如果您还没有机会升级到JDK 12,这将是非常棒的。

让我们看看谁应该考虑切换到它,以及为什么。

Shenandoah在G1上的关键进步是与应用程序线程同时执行更多的垃圾收集周期工作。G1只有在应用程序暂停时才能清空堆区域,即移动对象,而Shenandoah可以与应用程序同时重新定位对象。为了实现并发重新定位,它使用了一个被称为Brooks指针的东西。这个指针是Shenandoah堆中每个对象都拥有的附加字段,它指向对象本身。

Shenandoah这样做是因为当它移动一个对象时,它还需要修复堆中所有引用该对象的对象。当Shenandoah将对象移动到新位置时,它会保留旧的Brooks指针,将引用转发到对象的新位置。当引用对象时,应用程序将遵循指向新位置的转发指针。最终需要清理带有转发指针的旧对象,但通过将清理操作与移动对象本身的步骤分离,Shenandoah可以更轻松地完成对象的并发重新定位。

要在Java 12以后的应用程序中使用Shenandoah,请使用以下选项启用它:

-XX:+UnlockExperimentalVMOptions -XX:+UseShenandoahGC

如果您还不能跳转到Java12,但有兴趣尝试Shenandoah,那么可以使用Java8和Java11的后端口。值得注意的是,在Oracle提供的JDK版本中没有启用Shenandoah,但是其他OpenJDK发行商默认启用Shenandoah。更多关于Shenandoah的细节可以在JEP189中找到。

Shenandoah不是并发GCs的唯一选择。ZGC是OpenJDK附带的另一个GC(包括Oracle的版本),它在JDK 12中得到了改进。因此,如果您有一个应用程序存在垃圾收集暂停问题,并且您正在考虑尝试Shenandoah,那么您还应该看看ZGC,我们将在下面介绍它。

具有并发类卸载的ZGC

ZGC的主要目标是低延迟可扩展性易用性。为了实现这一点,ZGC允许Java应用程序在执行除线程堆栈扫描之外的所有垃圾收集操作时继续运行。它可以从几百MB扩展到TB大小的Java堆,同时始终保持非常低的暂停时间(通常在2毫秒内)。

可预测的低暂停时间对应用程序开发人员和系统架构师都有深远的影响。开发人员将不再需要担心设计复杂的方法来避免垃圾收集暂停。系统架构师不需要专门的GC性能调优专家来实现可靠的低暂停时间,这对于许多用例来说都非常重要。这使得ZGC非常适合需要大量内存的应用程序,例如大数据应用程序。但是,对于需要可预测且极低暂停时间的较小堆,ZGC也是一个很好的选择。

ZGC作为一个实验特性添加到JDK 11中。在JDK12中,ZGC增加了对并发类卸载的支持,允许Java应用程序在卸载未使用的类时继续运行,而不是暂停执行。

执行并发类卸载是复杂的,因此,类卸载传统上是在停止世界暂停时完成的。确定不再使用的类集需要首先执行引用处理。然后是终结器的处理——这就是我们如何引用Object.finalize()方法的实现。作为引用处理的一部分,必须遍历可从终结器访问的对象集,因为终结器可以通过无限链接链传递保持类的活动状态。不幸的是,访问可从终结器访问的所有对象可能需要很长时间。在最坏的情况下,整个Java堆可以通过单个终结器访问。ZGC与Java应用程序同时运行引用处理(自JDK11中引入ZGC以来)。

在引用处理完成后,ZGC知道哪些类不再需要。下一步是清理所有包含过时和无效数据的数据结构,因为这些类会消亡。清除从活动数据结构到无效或失效数据结构的链接。此取消链接操作需要遍历的数据结构包括几个内部JVM数据结构,例如代码缓存(包含所有JIT编译的代码)、类加载器数据图、字符串表、符号表、概要文件数据等。断开死数据结构的链接后,将再次遍历这些死数据结构以删除它们,从而最终回收内存。

到目前为止,所有JDK gc都在stop-the-world操作中完成了所有这一切,这给Java应用程序带来了延迟问题。对于低延迟GC,这是有问题的。因此,ZGC现在与Java应用程序同时运行所有这些功能,因此,不必为支持类卸载而支付延迟惩罚。事实上,为执行并发类卸载而引入的机制进一步改善了延迟。停止世界暂停以进行垃圾收集的时间现在只与应用程序中的线程数成比例。这种方法对暂停时间的显著影响如图所示。

JDK最新高速垃圾收集器概览

ZGC目前是Linux/x86 64位平台上的实验GC,从Java13开始,在Linux/Aarch上也是如此。您可以使用以下命令行选项启用它:

-XX:+UnlockExperimentalVMOptions -XX:+UseZGC

G1的改进

一些组织无法将其运行时系统更改为使用实验性GCs。他们会很高兴知道G1已经有了一些改进。G1收集器将其垃圾收集周期分为多个不同的暂停。

对象在分配后最初被认为是“年轻”一代的一部分。当它们在多个垃圾收集周期中保持活动时,它们最终会“保留”,然后被视为“旧的”。G1中的不同区域只包含一代对象,因此可以称为年轻区域或旧区域。

G1要达到暂停时间目标,需要能够确定在暂停时间目标内可以完成的工作块,并在暂停目标到期时完成该工作。G1有一套复杂的启发式方法来确定正确的工作量,这些启发式方法擅长预测所需的工作时间,但并不总是准确的。更复杂的是,G1不能只收集年轻区域的一部分;它在一次垃圾收集过程中收集所有年轻区域。

在Java12中,通过添加中止G1收集暂停的功能,这种情况得到了改善。G1跟踪其启发式算法预测要收集的区域数的准确程度,并仅在需要时进行可中止的垃圾收集。它将收集集(在一个周期内将被垃圾收集的区域集)拆分为两组:强制区域和可选区域。

强制区域始终在GC周期内收集。在时间允许的情况下收集可选区域,如果没有收集可选区域的时间用完,收集过程将中止。强制区域是所有年轻区域和一些潜在的旧区域。将旧代区域添加到此集合以响应两个条件。一些是为了确保可以继续疏散物体,另一些是为了使用预期的暂停时间。

通过将集合集合候选中的区域数除以-XX:G1MixedAccountTarget的值,计算要添加多少个区域的启发式操作将继续进行。如果G1预测还有时间收集更多的老一代区域,那么它也会向强制区域集添加更多区域,直到它预期使用80%的可用暂停时间。

这项工作的结果意味着G1能够中止或结束其混合GC循环。这将导致较低的GC暂停延迟和G1能够更频繁地实现其暂停时间目标的高概率。这一改进在JEP 344中有详细说明。

提示返回未使用的、已提交的内存

对Java最常见的批评之一是,它是一个内存猪——好吧,不再是了!有时,JVM通过命令行选项分配的内存超过了它们所需的内存;如果没有提供与内存相关的命令行选项,JVM可能会分配超出需要的内存。分配未使用的RAM会浪费金钱,特别是在云环境中,所有资源都经过适当的计量和成本计算。但是可以做些什么来解决这种情况,Java在资源消耗方面可以得到改进吗?

一种常见的情况是JVM必须处理的工作负载随时间而变化:有时需要更多内存,有时需要更少内存。在实践中,这通常是无关紧要的,因为JVM往往在启动时分配大量内存,甚至在不需要时也贪婪地保留着。在理想情况下,未使用的内存可以从JVM返回到操作系统,以便其他应用程序或容器能够使用它。从Java12开始,现在可以返回未使用的内存。

G1已经有能力释放未使用的内存,但它只在完全垃圾收集过程中这样做。完全垃圾收集过程通常是不常见的,也是不希望发生的,因为它们可能会导致长时间的停止,从而导致应用程序暂停。在JDK12中,G1获得了在并发垃圾收集过程中释放未使用内存的能力。此功能对于大多数空堆特别有用。当堆大部分为空时,GC周期可能需要一段时间来收集内存并将其返回到操作系统。为了确保内存迅速返回到操作系统,从Java 12开始,如果在G1PeriodicGCInterval参数在命令行上指定的时间段内没有发生垃圾收集周期,G1将尝试触发并发垃圾收集周期。然后,此并发垃圾收集周期将在周期结束时向操作系统释放内存。

为了确保这些定期并发垃圾收集过程不会增加不必要的CPU开销,它们仅在系统部分空闲时运行。用于触发并发循环是否运行的测量值是平均一分钟系统负载值,必须低于G1PeriodicGCSystemLoadThreshold指定的值。

更多详情请参见JEP 346:https://openjdk.java.net/jeps/346

结论

本文介绍了几种方法,您可以不用担心应用程序中GC导致的暂停时间。在G1继续改进的同时,随着堆大小的增加和暂停时间的可接受性的降低,新的GC(如Shenandoah和ZGC)提供了一个可扩展的、低暂停的未来。

 

除特别注明外,本站所有文章均为老K的Java博客原创,转载请注明出处来自https://javakk.com/2315.html

关于

发表评论

表情 格式

暂无评论

登录

忘记密码 ?

切换登录

注册