了解JDK的新型超快垃圾收集器:Shenandoah、ZGC和改进的G1 - oracle


ZGC,Shenandoah和对G1的改进使开发人员比以往任何时候都更接近无暂停时间。
在过去六个月中发生的一些最令人振奋的事态发展都在JDK的垃圾收集器(GC)的不断演进中,首先,我们将介绍Shenandoah,这是一种低延迟GC,主要与应用程序同时运行;我们还将介绍作为JDK 12的一部分发布的ZGC(Java 11中引入的低延迟并发GC)的最新改进;我们将详细解释从Java 9开始的作为默认GC的G1的两项改进。

Shenandoah
Shenandoah是作为JDK 12的一部分发布的新GC。实际上,Shenandoah的开发工作也向后移植了对JDK 8u和11u版本的改进。
Shenandoah团队已针对SpecJBB基准发布了暂停时间延迟的基准评分,该基准将延迟与JDK 9时代的G1进行了比较。结果有很大提升。

Shenandoah在G1之上的主要进步是在运行应用程序线程的同时完成了更多的垃圾回收周期工作。G1只能在应用程序暂停时撤离其堆区域(即移动对象),而Shenandoah可以与应用程序同时重新放置对象。
为了实现并发重定位,它使用了所谓的Brooks指针。该指针是Shenandoah堆中每个对象都具有的一个附加字段,它指向对象本身。Shenandoah之所以这样做是因为,当它移动一个对象时,它还需要修复堆中所有引用该对象的对象。当Shenandoah将对象移动到新位置时,它将旧的Brooks指针留在原处,从而将引用转发到该对象的新位置。当引用对象时,应用程序将转发指针跟随到新位置。最终,需要清除带有转发指针的旧对象,但是通过将清除操作与移动对象本身的步骤分离,Shenandoah可以更轻松地完成对象的并发重定位。
要从Java 12开始使用Shenandoah,请使用以下选项启用它:

-XX:+UnlockExperimentalVMOptions -XX:+UseShenandoahGC

如果您还不能升级到Java 12,但是您对试用Shenandoah感兴趣,那么可以使用向Java 8和Java 11的反向移植。值得注意的是,Oracle随附的JDK构建中未启用Shenandoah,但是其他OpenJDK发行商默认情况下启用了Shenandoah。
在并发GC方面,Shenandoah不是唯一的选择。ZGC是OpenJDK附带的另一个GC(包括Oracle的内部版本),并且已在JDK 12中进行了改进。因此,如果您的应用程序遇到垃圾回收暂停问题,并且您正在考虑尝试Shenandoah,则还应该注意在ZGC,我们将在下面进行介绍。

具有并发类卸载的ZGC
ZGC的主要目标是低延迟,可伸缩性和易用性。为此,ZGC允许Java应用程序在执行除线程堆栈扫描之外的所有其他垃圾回收操作时继续运行。它可以从数百MB扩展到TB大小的Java堆,同时始终保持非常低的暂停时间(通常在2 ms之内)。
对于应用程序开发人员和系统架构师而言,意想不到的低暂停时间可能会产生深远的影响。开发人员将不再需要担心设计复杂的方法来避免垃圾回收暂停。而且,系统架构师将不需要专业的GC性能调整专业知识来实现​​可靠的低暂停时间,这对于许多用例而言都是非常重要的。这使得ZGC非常适合需要大量内存(例如大数据)的应用程序。但是,对于需要可预测且极短的暂停时间的较小堆,ZGC也是不错的选择。
ZGC已作为实验功能添加到JDK 11中。在JDK 12中,ZGC添加了对并发类卸载的支持,从而允许Java应用程序在卸载未使用的类期间继续运行,而不是暂停执行。
执行并发的类卸载非常复杂,因此,传统上,类卸载是在整个JVM世界停下来的暂停中完成的。首先要确定不再使用的类集,首先需要执行引用处理。然后是终结器的处理-这就是我们指代该方法的实现的Object.finalize() 方式。作为引用处理的一部分,必须遍历终结器可访问的对象集,因为终结器可以通过无限制的链接链来传递类,使类保持活动状态。不幸的是,访问终结器可到达的所有对象可能需要很长时间。在最坏的情况下,可以通过单个终结器访问整个Java堆。ZGC与Java应用程序同时运行引用处理(因为JDK 11中引入了ZGC)。
引用处理完成后,ZGC知道不再需要哪些类。下一步是清除由于这些类死亡而导致的包含过时和无效数据的所有数据结构。清除从活动数据结构到无效或无效数据结构的链接。为此取消链接操作需要走的数据结构包括几个内部JVM数据结构,例如代码缓存(包含所有JIT编译的代码),类加载器数据图,字符串表,符号表,概要文件数据等。 。取消链接失效数据结构后,将再次遍历那些失效数据结构以将其删除,以便最终回收内存。
到现在为止,所有JDK GC都通过停止操作来完成所有这些操作,从而导致Java应用程序出现延迟问题。对于低延迟GC,这是有问题的。因此,ZGC现在与Java应用程序同时运行所有这些功能,因此,不因支持类卸载而造成任何延迟损失。实际上,引入的用于执行并发类卸载的机制进一步提高了延迟。

ZGC当前可作为实验性GC用于Linux / x86 64位平台,从Java 13开始,可在Linux / Aarch上使用。您可以使用以下命令行选项启用它:

-XX:+UnlockExperimentalVMOptions -XX:+UseZGC

G1改进
G1也取得了一些改进。G1收集器将其垃圾收集周期分为多个不同的暂停时间。
分配对象后,最初将对象视为“年轻”一代的一部分。当它们在多个垃圾回收周期中存活时,它们最终将“保有”,然后被视为“旧”。G1中的不同区域仅包含一代人的对象,因此可以称为年轻区域或旧区域。
为了使G1达到暂停时间目标,它需要能够识别出可以在暂停时间目标内完成的工作,并在暂停目标期满之前完成工作。G1具有一组复杂的启发式方法来识别正确的工作量,这些启发式方法擅长预测所需的工作时间,但它们并不总是准确的。G1不能仅收集年轻地区的一部分,这使情况更加复杂。它通过一次垃圾收集通行证收集了所有年轻地区。
在Java 12中,通过添加中止G1收集暂停的功能来改善了这种情况。G1跟踪其启发式算法预测要收集的区域数量的准确性,并在需要时仅进行可中止的垃圾收集。通过将收集集(将在一个周期中进行垃圾收集的区域集)分成两组进行处理:强制区域和可选区域。
强制性区域始终在GC周期内收集。在时间允许的情况下收集可选区域,如果在没有收集可选区域的情况下超时,则收集通道将中止。强制性区域是所有较年轻的区域,可能还有一些较旧的区域。将旧区域添加到此集中以响应两个标准。添加一些以确保可以继续疏散对象,而添加一些以耗尽预期的暂停时间。
启发式计算可通过将集合集合候选中的区域数除以的值来计算要添加的区域数-XX:G1MixedGCCountTarget。如果G1预测将有更多的时间来收集更多的旧区域,则它还将更多区域添加到强制区域集中,直到期望用尽可用暂停时间的80%。
这项工作的结果意味着G1可以中止或结束其混合GC循环。这导致较低的GC暂停等待时间,并且极有可能使G1更频繁地达到其暂停时间目标。JEP 344中详细介绍了这种改进。

迅速返回未使用的内存
对Java的最普遍的批评之一是它是一种内存消耗,现在不是了。
有时,通过命令行选项为JVM分配了超出其所需内存的内存。如果未提供与内存相关的命令行选项,则JVM可能分配了比所需更多的内存。分配未使用的RAM会浪费金钱,尤其是在对所有资源进行计量和适当计算的云环境中。但是,可以采取什么措施解决这种情况,并且可以在资源消耗方面改进Java?
常见的情况是JVM必须处理的工作负载随时间而变化:有时需要更多的内存,有时需要更少的内存。实际上,这通常是无关紧要的,因为JVM倾向于在启动时分配大量内存,即使在不需要时也会贪婪地保留它。在理想情况下,未使用的内存可以从JVM返回到操作系统,以便其他应用程序或容器可以使用它。

从Java 12开始,现在可以返回未使用的内存。

G1已经具有释放未使用的内存的功能,但只有在完整的垃圾回收过程中才可以释放它。完全垃圾收集通道通常很少发生,并且是不希望发生的,因为它们可能导致应用程序长时间停顿。在JDK 12中,G1获得了在并发垃圾回收过程中释放未使用内存的功能。此功能对于大多数为空的堆特别有用。当堆几乎为空时,GC周期可能需要一段时间才能获取内存并将其返回给操作系统。为了确保将内存迅速返回操作系统,从Java 12开始,如果在命令行指定的时间段内未发生垃圾回收周期,G1将尝试触发并发的垃圾回收周期。G1PeriodicGCInterval论点。然后,该并发的垃圾回收周期将在该周期结束时将内存释放给操作系统。
为了确保这些定期的并发垃圾收集传递不会增加不必要的CPU开销,它们仅在系统部分空闲时才运行。用于触发并发周期是否运行的度量是平均一分钟系统负载值,该值必须低于所指定的值G1PeriodicGCSystemLoadThreshold。