1.简介
如今,应用程序同时服务成千上万甚至数百万个用户的情况并不少见。这样的应用程序需要大量的内存。但是,管理所有这些内存可能很容易影响应用程序性能。
为了解决此问题,Java 11引入了Z垃圾收集器(ZGC)作为实验性垃圾收集器(GC)的实现。
在本教程中,我们将看到ZGC如何在多个TB的堆上设法保持低暂停时间
2.主要概念
要了解ZGC的工作原理,我们需要了解内存管理和垃圾回收器背后的基本概念和术语。
2.1 内存管理
物理内存是我们的硬件提供的RAM。
操作系统(OS)为每个应用程序分配虚拟内存空间。
当然,**我们将虚拟内存存储在物理内存中,并且操作系统负责维护两者之间的映射。**此映射通常涉及硬件加速。
2.2 多重映射
多重映射意味着虚拟内存中有特定的地址,这些地址指向物理内存中的相同地址。由于应用程序通过虚拟内存访问数据,因此它们对这种机制一无所知(并且不需要)。
实际上,我们将虚拟内存的多个范围映射到物理内存中的相同范围:
乍一看,它的用例并不明显,但稍后我们将看到ZGC需要它来发挥其魔力。而且,由于它分隔了应用程序的存储空间,因此它提供了一定的安全性。
2.3 搬迁
由于我们使用动态内存分配,因此随着时间的流逝,普通应用程序的内存会变得碎片化。这是因为当我们在内存中间释放一个对象时,那里会留有一定的可用空间。随着时间的流逝,这些间隙不断累积,我们的记忆将像是由自由空间和使用空间交替形成的棋盘。
当然,我们可以尝试用新对象填补这些空白。为此,我们应该在内存中扫描足够大的可用空间来容纳对象。这样做是一项昂贵的操作,尤其是如果我们每次要分配内存时都必须这样做。此外,内存仍将是零散的,因为可能无法找到具有所需大小的可用空间。因此,物体之间将存在间隙。当然,这些差距较小。另外,我们可以尝试最小化这些差距,但是它使用了更大的处理能力。
另一种策略是将对象从零散的内存区域频繁地重定位为更紧凑的格式。为了更有效,我们将内存空间划分为多个块。我们将所有对象重新放置在一个块中,或者不重新放置。这样,由于我们知道内存中有整个空块,因此内存分配将更快。
2.4 垃圾收集
创建Java应用程序时,我们不必释放分配的内存,因为垃圾回收器会为我们做这件事。总之,GC通过一系列引用监视我们可以从应用程序中访问哪些对象,并释放我们无法访问的对象。
GC需要跟踪堆空间中对象的状态才能完成其工作。例如,可能的状态是可到达的。这意味着应用程序拥有对该对象的引用。此引用可能是可传递的。唯一重要的是应用程序可以通过引用访问这些对象。另一个例子是可终结的:我们无法访问的对象。这些是我们认为是垃圾的对象。
为此,垃圾收集器具有多个阶段。
2.5 GC相属性
GC相可以具有不同的属性:
- 一个平行相位可在多个GC线程运行
- 一个串行阶段运行在单个线程
- 一个停止的世界相不能与应用程序代码同时运行
- 一个并发的阶段可以在后台运行,而我们的应用程序不工作
- 一个增量阶段可以完成所有的工作之前终止以后继续它
请注意,上述所有技术都有其优点和缺点。例如,假设我们有一个可以与我们的应用程序同时运行的阶段。此阶段的串行实现需要占整体CPU性能的1%,并运行1000ms。相反,并行实现使用30%的CPU,并在50ms内完成其工作。
在此示例中,并行解决方案总体上使用更多的CPU,因为它可能更复杂并且必须同步线程。对于CPU繁重的应用程序(例如批处理作业),这是一个问题,因为我们执行有用工作的计算能力较低。
当然,这个例子有虚构的数字。但是,很明显,所有应用程序都有其特征,因此它们具有不同的GC要求。
3. ZGC概念
ZGC打算提供尽可能短的停止生产阶段。它以这样的方式实现它:这些暂停时间的持续时间不会随着堆大小的增加而增加。这些特性使ZGC非常适合服务器应用程序,在这些应用程序中,大堆很常见,因此要求快速的应用程序响应时间。
在久经考验的GC技术之上,ZGC引入了新概念,我们将在以下各节中进行介绍。
现在,让我们看一下ZGC工作原理的整体情况。
3.1 大图景
ZGC有一个称为标记的阶段,我们在其中找到可到达的对象。GC可以通过多种方式存储对象状态信息。例如,我们可以创建一个*Map,*其中的键是内存地址,值是该地址处对象的状态。这很简单,但是需要额外的内存来存储此信息。而且,维护这样的地图可能具有挑战性。
**ZGC使用另一种方法:将参考状态存储为参考的位。**这称为参考着色。但是这样,我们面临着新的挑战。将引用的位设置为存储有关对象的元数据意味着,由于状态位不保存有关对象位置的任何信息,因此多个引用可以指向同一对象。多图救援!
我们还希望减少内存碎片。ZGC使用重定位来实现此目的。但是,对于大堆来说,重定位是一个缓慢的过程。**由于ZGC不需要很长的暂停时间,因此它与应用程序并行地执行大多数重定位。**但这引入了新的问题。
假设我们有一个对象的引用。ZGC重新定位它,然后发生上下文切换,应用程序线程在其中运行并尝试通过其旧地址访问该对象。ZGC使用负载屏障来解决此问题。负载屏障是一段代码,当线程从堆中加载引用时(例如,当我们访问对象的非原始字段时),该代码就会运行。
在ZGC中,负载屏障会检查参考的元数据位。根据这些位,**ZGC可能会在获取参考之前对参考执行一些处理。**因此,它可能会产生完全不同的参考。我们称此为重映射。
3.2 打标
ZGC将标记分为三个阶段。
第一阶段是世界停止阶段。在此阶段,我们寻找根引用并对其进行标记。**根引用是到达堆中对象(**例如局部变量或静态字段)的起点。由于根引用的数量通常很少,因此此阶段很短。
下一阶段是并发。在此阶段,**我们从根引用开始遍历对象图。我们标记我们到达的每个物体。**同样,当负载屏障检测到未标记的参考时,也会对其进行标记。
最后一个阶段也是“停滞世界”阶段,用于处理一些边缘情况,例如弱引用。
至此,我们知道可以到达哪些对象。
ZGC使用marked0和marked1元数据位进行标记。
3.3 参考色
引用表示虚拟内存中字节的位置。但是,我们不必一定要使用引用的所有位来执行操作-有些位可以表示引用的属性。这就是我们所说的参考色。
使用32位,我们可以寻址4 GB。由于当今计算机的内存要比这更多,所以我们显然不能使用这32位中的任何一个进行着色。因此,ZGC使用64位引用。这意味着ZGC仅在64位平台上可用:
ZGC引用使用42位来表示地址本身。结果,ZGC引用可以寻址4 TB的内存空间。
最重要的是,我们有4位存储参考状态:
- 可终结位–对象只能通过终结器访问
- 重新映射位-引用是最新的,指向对象的当前位置(请参见重定位)
- marked0和marked1位–用于标记可达对象
我们也将这些位称为元数据位。在ZGC中,这些元数据位恰好是1。
3.4 搬迁
在ZGC中,重定位包括以下阶段:
- 一个并发阶段,它查找块,我们要重定位并将它们放入重定位集中。
- 停止世界阶段将所有根引用重定位到重定位集中并更新其引用。
- 并发阶段重定位重定位集中的所有其余对象,并将旧地址和新地址之间的映射存储在转发表中。
- 其余参考的重写将在下一个标记阶段进行。这样,我们不必遍历对象树两次。另外,负载屏障也可以做到。
3.5 重新映射和加载障碍
请注意,在重定位阶段,我们没有将大多数引用重写为重定位地址。因此,使用这些引用,我们将无法访问想要的对象。更糟糕的是,我们可以访问垃圾。
ZGC使用负载屏障来解决此问题。负载屏障使用一种称为重新映射的技术来修复指向重定位对象的引用。
当应用程序加载引用时,它将触发负载屏障,然后,屏障将按照以下步骤返回正确的引用:
- 检查重映射位是否设置为1。如果是,则表示引用是最新的,因此我们可以安全地返回它。
- 然后,我们检查所引用的对象是否在重定位集中。如果不是,那意味着我们不想重新安置它。为了避免下一次加载此引用时进行此检查,我们将重映射位设置 为1并返回更新的引用。
- 现在我们知道我们要访问的对象是重定位的目标。唯一的问题是搬迁是否发生?如果对象已重定位,则跳到下一步。否则,我们现在将其重新放置,并在转发表中创建一个条目,该条目存储每个重新放置的对象的新地址。此后,我们继续下一步。
- 现在我们知道对象已重定位。要么是ZGC,要么是上一步中的操作,要么是此对象的较早命中时的负载屏障。我们将此引用更新为对象的新位置(使用上一步中的地址或通过在转发表中查找该地址),设置重映射位,然后返回引用。
就是这样,通过上面的步骤,我们确保了每次尝试访问对象时,我们都会获得对它的最新引用。由于每次我们加载参考时,都会触发加载障碍。因此,它降低了应用程序的性能。特别是第一次访问重定位的对象时。但这是我们要缩短暂停时间必须付出的代价。而且由于这些步骤相对较快,因此不会显着影响应用程序性能。
4.如何启用ZGC?
在运行应用程序时,我们可以使用以下命令行选项启用ZGC:
-XX:+UnlockExperimentalVMOptions -XX:+UseZGC
请注意,由于ZGC是实验性GC,因此需要一段时间才能获得正式支持。
5.结论
在本文中,我们看到ZGC打算以较短的应用程序暂停时间来支持大堆大小。
为了达到这个目标,它使用了一些技术,包括彩色的64位引用,负载屏障,重定位和重新映射。
本文部分内容翻译自国外文章
涉及到原作者:AttilaFejér、Chris Stewart 等