ZGC简介-Java快速进阶教程
. 简介
如今,应用程序同时为数百万甚至数亿用户提供服务的情况并不少见。此类应用程序需要大量内存。但是,管理所有这些内存可能很容易影响应用程序性能。
为了解决这个问题,Java 11引入了Z垃圾收集器(ZGC)作为实验性垃圾收集器(GC)实现。
在本教程中,我们将看到ZGC 如何设法在数 TB 的堆上保持较短的暂停时间。
若想深入学习相关内容请自行查阅内容
Java字节码-Java快速进阶教程
什么是 Java 编译器-Java快速进阶教程
什么是 Java 中的 JVM-Java快速进阶教程
什么是 JRE-Java快速进阶教程
java垃圾回收机制GC(Garbage Collection)-Java快速进阶教程
JVM规范定义运行时数据区详解-Java快速进阶教程
Java 中的堆栈内存和堆空间介绍-Java快速进阶教程
2. 主要概念
要了解ZGC的工作原理,我们需要了解内存管理和垃圾收集器背后的基本概念和术语。
2.1. 内存管理
物理内存是我们的硬件提供的 RAM。
操作系统 (OS) 为每个应用程序分配虚拟内存空间。
当然,我们将虚拟内存存储在物理内存中,操作系统负责维护两者之间的映射。此映射通常涉及硬件加速。
2.2. 多重映射
多重映射意味着虚拟内存中存在特定地址,这些地址指向物理内存中的相同地址。由于应用程序通过虚拟内存访问数据,因此它们对此机制一无所知(也不需要)。
实际上,我们将虚拟内存的多个范围映射到物理内存中的相同范围:
乍一看,它的用例并不明显,但我们稍后会看到,ZGC需要它来发挥它的魔力。此外,它还提供了一些安全性,因为它分隔了应用程序的内存空间。
2.3. 重新定位
由于我们使用动态内存分配,因此普通应用程序的内存会随着时间的推移而变得碎片化。这是因为当我们释放内存中间的物体时,自由空间的间隙仍然存在。随着时间的推移,这些间隙会累积,我们的记忆将看起来像一个由可用和已用空间的交替区域组成的棋盘。
当然,我们可以尝试用新对象来填补这些空白。为此,我们应该扫描内存以查找足够大的可用空间来容纳我们的对象。这样做是一项昂贵的操作,特别是如果我们每次要分配内存时都必须这样做。此外,内存仍将是碎片化的,因为我们可能无法找到具有所需确切大小的可用空间。因此,对象之间会有间隙。当然,这些差距较小。此外,我们可以尝试最小化这些差距,但它会消耗更多的处理能力。
另一种策略是以更紧凑的格式频繁地将对象从碎片内存区域重新定位到可用区域。为了更有效,我们将内存空间分成块。我们重新定位块中的所有对象,或者不重新定位任何对象。这样,内存分配会更快,因为我们知道内存中有整个空块。
2.4. 垃圾回收
当我们创建一个 Java 应用程序时,我们不必释放我们分配的内存,因为垃圾收集器会为我们完成这项工作。总之,GC 通过引用链监视我们可以从应用程序中访问哪些对象,并释放我们无法访问的对象。
GC 需要跟踪堆空间中对象的状态才能完成其工作。例如,可能的状态是可访问的。这意味着应用程序保存对对象的引用。此引用可能是可传递的。唯一重要的是应用程序可以通过引用访问这些对象。另一个例子是可以完成的:我们无法访问的对象。这些是我们认为是垃圾的物体。
为了实现这一点,垃圾回收器有多个阶段。
2.5. GC阶段属性
GC阶段可以具有不同的属性:
- 并行阶段可以在多个 GC 线程上运行
- 串行阶段在单个线程上运行
- 停止阶段不能与应用程序代码同时运行
- 并发阶段可以在后台运行,而我们的应用程序则完成其工作
- 增量阶段可以在完成其所有工作之前终止,并在以后继续
请注意,上述所有技术都有其优点和缺点。例如,假设我们有一个可以与我们的应用程序同时运行的阶段。此阶段的串行实现需要 1% 的整体 CPU 性能,运行时间为 1000 毫秒。相比之下,并行实现利用 30% 的 CPU 并在 50 毫秒内完成其工作。
在此示例中,并行解决方案总体上使用更多的 CPU,因为它可能更复杂并且必须同步线程。对于 CPU 密集型应用程序(例如,批处理作业),这是一个问题,因为我们的计算能力较低,无法完成有用的工作。
当然,这个例子是虚构的数字。但是,很明显,所有应用程序都有其特征,因此它们具有不同的GC要求。
3. ZGC概念
ZGC打算提供尽可能短的“全局停止”阶段。它通过这样一种方式实现,即这些暂停时间的持续时间不会随着堆大小的增加而增加。这些特性使ZGC非常适合服务器应用程序,在服务器应用程序中,大堆是常见的,需要快速的应用程序响应时间。
在经过试验和测试的GC技术之上,ZGC引入了新的概念,我们将在下面几节中介绍这些概念。
但现在,让我们来看看ZGC工作原理的整体情况。
3.1. 初始
ZGC有一个称为标记的阶段,我们在其中找到可到达的对象。GC 可以通过多种方式存储对象状态信息。例如,我们可以创建一个 Map,其中键是内存地址,值是对象在该地址的状态。这很简单,但需要额外的内存来存储此信息。此外,维护这样的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 位来存储引用状态:
- finalizable位 – 对象只能通过终结器访问
- remap位 – 参照是最新的,并指向对象的当前位置(请参见重映射位)
- 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 位引用、负载屏障、重定位和重新映射。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
· 上周热点回顾(2.17-2.23)