JDK 16的新增功能:ZGC


JDK 16已经发布,并且像往常一样,每个新发行版都具有许多新功能,增强功能和错误修复。 ZGC获得了 46个增强功能 和25个错误修复。在这里,我将介绍一些更有趣的增强功能。
 
最大亚毫秒暂停
又名并发线程堆栈处理,当我们开始ZGC项目时,我们的目标是决不让GC暂停花费超过10毫秒的时间。当时10毫秒似乎是一个雄心勃勃的目标。那时,HotSpot缺乏同时执行此操作所需的大量基础结构,因此,花了几年的时间才能达到目标。
达到最初的10ms目标后,我们重新瞄准目标,并将目标设定在更具野心的目标上。也就是说,GC暂停不得超过1ms。从JDK 16开始,我很高兴地报告我们也已经实现了这一目标。
ZGC现在具有O(1)个 暂停时间。换句话说,它们以恒定的时间执行,并且不会随堆,活动集或根集大小(或与此相关的任何其他内容)的增加而增加。
当然,我们仍然要依靠操作系统调度程序来分配GC线程的CPU时间。但是,只要您的系统没有严重超额配置,您就可以期望看到平均GC暂停时间约为0.05毫秒(50微秒),最大暂停时间约为0.5毫秒(500微秒)。
 
如何做到?
那么,我们是怎么做到的?好吧,在JDK 16之前,ZGC暂停时间仍然根据根集(的子集)的大小进行缩放。更准确地说,我们仍然在“世界停止”阶段扫描线程堆栈。这意味着,如果Java应用程序具有大量线程,则暂停时间将增加。如果这些线程具有深层调用堆栈,则暂停时间将增加更多。从JDK 16开始,线程堆栈的扫描是同时进行的,即Java应用程序继续运行。
详细点击标题见原文
 
就地重分配
在JDK 16中,ZGC支持就地重分配。此功能有助于在堆已填满边缘时GC需要收集垃圾时避免OutOfMemoryError。通常,ZGC通过将对象从稀疏填充的堆区域移动到一个或多个空堆区域中来压缩堆(从而释放内存),在这些区域中可以密集地打包这些对象。该策略简单明了,非常适合并行处理。但是,它有一个缺点。它需要一定数量的可用内存(每种大小类型至少有一个空堆区域)才能开始重定位过程。如果堆已满,即所有堆区域都已被使用,则我们无处可移动对象。
在JDK 16之前,ZGC通过保留堆来解决此问题。该堆保留是一组堆区域,这些堆区域已被预留,无法用于Java线程的常规分配。相反,在重定位对象时,仅允许GC本身使用堆保留。这确保了空的堆区域可用,即使从Java线程的角度来看堆已满,也可以启动重定位过程。堆保留通常仅占堆的一小部分。
 
转发表的分配和初始化
当ZGC重定位对象时,该对象的新地址记录在转发表中,该表是在Java堆之外分配的数据结构。每个选择为重定位集(压缩以释放内存的堆区域的集合)的一部分的堆区域都将获得与之关联的转发表。
在JDK 16之前,当重定位集非常大时,转发表的分配和初始化可能会占用整个GC周期时间的很大一部分。重定位集的大小与在重定位期间移动的对象数相关。例如,如果您有一个大于100GB的堆,并且工作负载导致大量碎片,并且小洞均匀分布在整个堆中,那么重定位集将很大,并且分配/初始化它可能需要一段时间。当然,这项工作总是在并发阶段完成的,因此它从未影响过GC的暂停时间。尽管如此,这里仍有改进的空间。
在JDK 16中,ZGC现在批量分配转发表。现在,我们不做任何调用(可能成千上万个)为malloc/new为每个表分配内存的方式,而是通过一次调用来一次性分配所有表所需的所有内存。这有助于避免通常的分配开销和潜在的锁争用,并显着减少分配这些表所需的时间。
 
概括

  • 通过并发线程堆栈扫描,ZGC现在在微秒范围内具有暂停时间,平均暂停时间为〜50µs,最大暂停时间为〜500µs。暂停时间不受堆,活动集和根集大小的影响。
  • 堆储备现在已经不复存在了,ZGC在需要时就地重新分配。这样可以节省内存,但也可以确保在所有情况下都可以成功压缩堆。
  • 现在可以更有效地分配和初始化转发表,这缩短了完成GC周期所需的时间,尤其是在收集稀疏填充的大堆时。

有关ZGC的更多信息,请参见OpenJDK WikiInside Java的GC部分 或此博客