1.简介
今天,应用程序同时为数千甚至数百万用户提供服务的情况并不少见。这些应用程序通常需要大量内存。但不得不小心的是,内存管理非常容易影响应用程序性能。
为了解决这个问题,Java 11引入了Z垃圾收集器(ZGC)作为垃圾收集器(GC)实现(2019.05.15注:当前这个功能为实验性)。
在本教程中,我们将看到ZGC如何设法在数TB的内存堆上保持低暂停时间。
2.主要概念
为了理解ZGC,我们需要了解背后的基本概念和术语内存管理和垃圾收集器。
2.1。内存管理
物理内存是我们的硬件提供的RAM。
操作系统(OS)为每个应用分配虚拟内存空间。
当然,我们将虚拟内存存储在物理内存中,操作系统负责维护两者之间的映射。此映射通常涉及硬件加速。
2.2。垃圾收集
当我们创建Java应用程序时,我们不必释放我们分配的内存,因为垃圾收集器会为我们执行此操作。综上所述,GC可以通过一系列的内存引用识别判断应用程序中的哪些对象可达,从而释放不可达的对象。
为实现这一目标,垃圾收集器有多个阶段。
2.3。 GC相属性
GC阶段可以具有不同的属性:
- 并行阶段:可以在多个GC线程上运行
- 串行阶段:单个线程上运行
- Stop-the-world阶段:GC线程不能与应用程序代码同时运行
- 并发阶段:GC线程可以在后台运行,而应用程序可以完成它的工作
- 渐进式阶段:GC可以在完成所有工作之前终止,并在以后继续
请注意,所有上述技术都有其优点和缺点。例如,假设我们有一个可以与我们的应用程序同时运行的阶段。此阶段的串行实现需要1%的整体CPU性能并运行1000ms。相比之下,并行实现使用30%的CPU并在50ms内完成其工作。
在这个例子中,并行解决方案使用更多的CPU资源,因为它可能更复杂并且必须同步线程。对于CPU密集型应用程序(例如批处理作业)来说,这是一个问题,因为一旦CPU计算能力较低时导致无法进行有效的工作。
当然,这个例子的数字可能不是真实的。但是,显而易见所有应用程序都有自己的特性,因此它们有不同的GC要求。
有关更详细的说明,请访问我们关于Java内存管理的文章。
3. ZGC Concepts
除了久经考验的GC技术之外,ZGC还引入了两个新概念:指针着色和Load barriers。
3.1。指针着色
指针表示虚拟内存中字节的位置。但是,我们不一定要使用指针的所有位来执行此操作 – 一些位可以表示指针的属性。这就是我们所说的指针着色。
使用32位,我们可以处理4千兆字节。由于现在配置的内存比这个更常见,我们显然不能使用这32位中的任何一个进行着色。因此,ZGC使用64位指针。这意味着ZGC仅适用于64位平台:
ZGC指针使用42位来表示地址本身。因此,ZGC指针可以处理4TB的内存空间。
最重要的是,我们有4位来存储指针状态:
- 终结bit – 只能通过终结器访问该对象
- 重映射bit – 引用指向对象的当前地址(请参阅重定位)
- marked0和marked1bits – 用于标记对象可达性
我们还将这些位称为元数据位。在ZGC中,这些元数据位中只有一个是1。
3.2。 Multi-Mapping
Multi-mapping意味着我们将多个虚拟内存范围映射到物理内存。在ZGC中,这些范围仅在前面提到的元数据位中不同。
请注意指针着色解除引用更加昂贵因为我们必须屏蔽有用位来访问地址本身。然而,ZGC绕过这个成本,因为四个元数据位中只有一个是1。这样我们只有四个范围要映射,映射由操作系统处理。此外,我们只使用其中三个范围,因为我们从不想取消引用可终结指针:
3.3。Load Barriers
Load barriers是一段运行代码(当线程从堆中载入引用时) – 例如,当我们访问对象的non-primitive字段时。
在ZGC中,load barriers检查引用的元数据位。根据这些位,ZGC可能会在获得引用前执行一些处理。因此,它可能产生完全不同的引用。
3.4。标记
标记是垃圾收集器确定对象是否可达的过程。不可达对象被认为是需要回收的对象。 ZGC将标记分为三个阶段:
第一阶段是Stop-the-world阶段。在这个阶段,我们寻找根引用并标记它们。根引用是到达堆中对象的起点例如,局部变量或静态字段。由于根引用的数量通常较小,因此该阶段很短。
下一阶段是并发阶段。在这个阶段,我们遍历对象图,从根引用开始。我们标记我们到达的每个对象。此外,当Load Barriers检测到未标记的引用时,也会标记它。
最后阶段也是stop-the-world阶段,用于处理一些边缘问题,比如弱引用。
此时,我们知道哪些对象是可达的。
ZGC使用marked0和marked1用于标记的元数据位。
3.5。重定位
当我们必须为新对象分配内存时,我们可以遵循两种策略。
首先,我们可以扫描内存中的可用空间,该空间足以容纳我们的对象。扫描内存是一项昂贵的操作。此外,内存将被分段,因为对象之间会有间隙。如果我们想要最小化这些差距,它会使用更多的处理能力。
另一个策略是经常将碎片存储区中的对象重定位到更紧凑格式的空闲区域。为了更有效,我们将内存空间分成块。我们重新定位块中的所有对象或不重定位。这样,内存分配会更快,因为我们知道内存中有整个空块。
在ZGC,重定位也包括三个阶段。
- 并发阶段:查找我们要重定位的块并将它们放入重定位集中。
- Stop-the-world阶段:重定位重定位集中的所有根引用并更新其引用。
- 并发阶段:重定位重定位集中的所有剩余对象,并在转发表中存储旧地址和新地址之间的映射。
3.6。重新映射
请注意,在重定位阶段,我们没有重写对重定位对象的所有引用。因此,使用这些引用,我们将无法访问我们想要的对象。更糟糕的是,我们可能会访问垃圾。
ZGC使用load barriers来解决这个问题。Load barriers使用称为重新映射的技术来修复指向重定位对象的引用。
下图显示了重映射的工作原理:
4.如何启用ZGC?
运行我们的应用程序时,我们可以使用以下命令行选项启用ZGC:
-XX:+UnlockExperimentalVMOptions -XX:+UseZGC
请注意,由于ZGC是一个实验性GC,因此需要一些时间才能获得官方支持。
5.结论
在本文中,我们看到ZGC打算以较低的应用程序暂停时间支持超大堆内存GC管理。
为了实现这一目标,它使用了包括彩色64位指针,Load barriers,重定位和重新映射在内的技术。
参考资料