ZGC:可扩展的低延迟JVM垃圾收集器

作者:享学课堂Mark老师

转载请声明出处!

1.简介

今天,应用程序同时为数千甚至数百万用户提供服务的情况并不少见。这些应用程序需要大量内存。但是,管理所有内存可能会轻易影响应用程序性能。

为了解决这个问题,Java 11引入了Z垃圾收集器(ZGC)作为实验性垃圾收集器(GC)实现。

我们来看看ZGC如何设法在几TB的堆上保持低暂停时间。

2.主要概念

为了理解ZGC,我们需要了解内存管理和垃圾收集器背后的基本概念和术语。

2.1 内存管理

物理内存是我们硬件提供的RAM。

操作系统(OS)为每个应用程序分配虚拟内存空间。

当然,我们将虚拟内存存储在物理内存中,操作系统负责维护两者之间的映射。此映射通常涉及硬件加速。

2.2 垃圾收集

当我们创建Java应用程序时,我们不必释放我们分配的内存,因为垃圾收集器会为我们执行此操作。总之,GC可以通过一系列参考资料从我们的应用程序中获取哪些对象,从而释放出我们无法触及的对象。

为实现这一目标,垃圾收集器有多个阶段。

2.3 GC阶段的属性

GC阶段可以具有不同的属性:

  • 并行阶段可以在多个GC线程上运行
  • 串行阶段在单个线程上运行
  • 停止世界阶段不能与应用程序代码同时运行
  • 并发阶段可以在后台运行,而我们的应用程序可以完成它的工作
  • 增量阶段可以在完成所有工作之前终止,并在以后继续

所有上述技术都有其优点和缺点。例如,假设我们有一个可以与我们的应用程序同时运行的阶段。此阶段的串行实现需要1%的整体CPU性能并运行1000ms。相比之下,并行实现使用30%的CPU并在50ms内完成其工作。

在此示例中,并行解决方案使用更多的CPU,因为它可能更复杂并且必须同步线程。对于CPU升沉应用程序(例如批处理作业)来说,这是一个问题,因为我们的计算能力较低,无法进行有效的工作。

当然,这个例子有编号。但是,很明显所有应用程序都有自己的特性,因此它们具有不同的GC要求。

有关更详细的描述,请访问我们关于Java内存管理的文章。

3.ZGC Concepts

除了久经考验的GC技术之外,ZGC还引入了两个新概念:指针着色和负载屏障。

3.1 指针着色

指针表示虚拟内存中字节的位置。但是,我们不一定要使用指针的所有位来执行此操作 - 某些位可以表示指针的属性。这就是我们所说的指针着色。

使用32位,我们可以处理4GB字节。由于现在配置的内存已经远远超过了这个数量,我们显然不能使用32位。因此,ZGC使用64位指针, 这意味着ZGC仅适用于64位平台:

ZGC指针使用42位来表示地址本身。因此,ZGC指针可以处理4TB的内存空间。

最重要的是,我们有4位来存储指针状态:

  • finalizable bit - 该对象只能通过终结器来访问
  • 重映射位 - 参考指向对象的当前地址(请参阅 3.5重定位)
  • marked0和marked1位 - 这些用于标记可到达的对象

我们还将这些位称为元数据位。

3.2 多重映射

多重映射意味着我们将多个虚拟内存范围映射到物理内存。在ZGC中,这些范围仅在前面提到的元数据位中不同。

指针着色使解除引用开销更加昂贵,因为我们必须屏蔽有用的位来访问地址本身。但是,ZGC绕过这个成本,因为四个元数据位中只有一个是1。这样我们只有四个范围要映射,映射由操作系统处理。此外,我们只使用其中三个范围,因为我们从不想取消引用可终结指针:

3.3 负载屏障

负载屏障是一个代码片段,它在线程从堆加载引用时运行 - 例如,当我们访问对象的非基本字段时。

在ZGC中,负载障碍检查引用的元数据位。根据这些位,ZGC可能会在我们获得它之前对引用执行一些处理。因此,它可能产生完全不同的引用。

3.4 标记

标记是垃圾收集器确定我们可以到达哪些对象的过程。我们无法达到的被认为是垃圾。ZGC将标记分为三个阶段:

第一阶段是Stop The World阶段。在这个阶段,我们寻找根引用并标记它们。根引用是到达堆中对象的起点,例如,局部变量或静态字段。由于根引用的数量通常较小,因此该阶段很短。

下一阶段是并发阶段。在这个阶段,我们从根引用开始遍历对象图。我们标记我们到达的每个对象。此外,当负载屏障检测到未标记的引用时,也会进行标记。

最后阶段也是Stop The World阶段,用来处理一些边缘情况,比如弱引用。

这几个阶段完成后,我们就知道哪些对象可达,哪些对象不可达。

ZGC使用marked0和marked1元数据位进行标记。

3.5 重定位

当我们必须为新对象分配内存时,我们可以遵循两种策略。

首先,我们可以扫描内存中的可用空间,直到有空间间足以容纳我们的对象。但是扫描内存是一项昂贵的操作,此外,内存将变得碎片化。如果我们想要减小碎片化,让内存变得更紧凑,将消耗更多的CPU处理能力。

另一种策略是频繁地将碎片存储区域中的对象以更紧凑的格式重定位到空闲区域。为了更有效,我们将内存空间分成块。我们要么将所有对象重新定位到一个块中的或者一个块不存在一个对象。这样,内存分配会更快,因为我们知道内存中有整个空块。

在ZGC,重定位也包括三个阶段。

  • 并发阶段查找我们要重定位的块并将它们放入重定位集中。
  • Stop The World阶段将重定位集中的所有根引用进行重定位并更新其引用。
  • 并发阶段将重定位集中的所有剩余对象进行重定位,并在转发表中存储旧地址和新地址之间的映射。

3.6 重新映射

请注意,在重定位阶段,我们没有重写对重定位对象的所有引用。因此,使用这些引用,我们将无法访问我们想要的对象。更糟糕的是,我们会访问到垃圾对象。

ZGC使用负载屏障来解决这个问题。负载屏障使用称为重新映射的技术来修复指向重定位对象的引用。

4.如何启用ZGC?

运行我们的应用程序时,我们可以使用以下命令行选项启用ZGC:

-XX:+ UnlockExperimentalVMOptions -XX:+ UseZGC 请注意,目前ZGC是一个实验性GC,在生产平台上使用,还需要再考察。

5.结论

ZGC打算以较低的应用程序暂停时间支持大堆。

为了实现这一目标,它使用了包括有色64位指针,负载屏障,重定位和重新映射在内的技术。

持续关注我,分享更多干货。

你可能感兴趣的:(Java)