首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >ZGC 入门简介:可扩展低延迟 JVM 垃圾收集器

ZGC 入门简介:可扩展低延迟 JVM 垃圾收集器

作者头像
用户9421738
发布于 2025-06-13 06:59:04
发布于 2025-06-13 06:59:04
12500
代码可运行
举报
文章被收录于专栏:大数据从业者大数据从业者
运行总次数:0
代码可运行

1. 引言

高并发应用程序通常需要大量内存,而如何管理这些内存确实是个难题,JDK 11 引入ZGC垃圾收集器来解决这个难题,JDK15已达到生产级别。

2. 主要概念

2.1 内存管理

所谓物理内存就是硬件RAM。而操作系统为每个应用程序分配虚拟内存。虚拟内存是存储在物理内存中,并由操作系统负责维护两者映射。

2.2 多重映射

多重映射意味着虚拟内存中存在特定地址指向物理内存中的地址。应用程序通过虚拟内存访问数据,对这种机制无感知。实际上就是将虚拟内存的多个范围映射到物理内存中的同一范围:

2.3 重定位

由于使用动态内存分配,应用程序的内存会随着时间的推移而变得碎片化。究其原因就是当释放内存中间的一个对象时会留下一个空闲空间的间隙。随着时间推移,这些间隙会累积,内存看起来就像一个由空闲和已用空间交替组成的棋盘。

当然,可以尝试用新对象填充这些间隙。那就需要扫描内存以寻找足够大的空闲空间来容纳新对象。这种操作开销很大,尤其是每次分配内存时都必须这样做时。此外,内存仍然会碎片化,因为可能无法找到与所需大小完全匹配的空闲空间,因此对象之间仍会存在间隙。当然,这些间隙会更小。也可以尝试最小化这些间隙,但会消耗更多处理能力。

另一种策略是频繁地将对象从碎片化的内存区域重定位到更紧凑的空闲区域。为了更有效,可以将内存空间划分为块。以块为单位重定位,内存分配会更快。

2.4 垃圾收集

Java 应用程序不必负责释放已分配的内存,而是交给垃圾收集器完成。简而言之,GC 会观察哪些对象可以通过引用链从应用程序访问到,并释放那些无法访问到的对象。GC 需要跟踪堆空间中对象的状态来完成。例如,一个可能的状态是 “可到达”,这意味着应用程序持有对该对象的引用,该引用可能是传递性的,关键是应用程序可以通过引用访问这些对象。另一个例子是 “可终结”:就是无法访问的对象,这些对象被视为垃圾。

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

2.5 GC 阶段属性

  • 并行阶段:可以在多个 GC 线程上运行
  • 串行阶段:在单个线程上运行
  • 全局停顿阶段:无法与应用程序代码并发运行
  • 并发阶段:可以在后台运行,而我们的应用程序执行其工作
  • 增量阶段:可以在完成所有工作之前终止,并在以后继续

注意,上述技术都有其优缺点。假设有一个可以与应用程序并发运行的阶段。该阶段的串行实现需要占用总 CPU 性能的 1%,并运行 1000 毫秒。相比之下,并行实现利用 30% 的 CPU,并在 50 毫秒内完成其工作。

在这个例子中,并行解决方案总体上使用了更多的 CPU,因为它可能更复杂,并且必须同步线程。对于 CPU 密集型应用程序(例如批处理作业),这是一个问题,因为用于执行有用工作的计算能力较少。

3. ZGC 概念

ZGC旨在提供尽可能短的全局停顿阶段,其实现方式是使这些暂停时间的持续时间不会随着堆大小的增加而增加。这些特性使 ZGC 非常适合服务器应用程序,因为在服务器应用程序中大堆很常见,并且需要快速的应用程序响应时间。

3.1 总体概述

ZGC 有一个称为 “标记” 的阶段,用于查找可到达的对象。GC 可以通过多种方式存储对象状态信息。例如,可以创建一个 Map,其中键是内存地址,值是该地址处对象的状态。这很简单,但需要额外的内存来存储此信息,此外,维护这样的映射可能具有挑战性。

ZGC 使用不同的方法:它将引用状态存储为引用的位,这称为 “引用着色”。但这样我们就面临一个新的挑战,将引用的位设置为存储对象的元数据意味着多个引用可以指向同一个对象,因为状态位不包含有关对象位置的任何信息。多重映射来解决这个问题!

我们还希望减少内存碎片,ZGC 通过重定位来实现这一点。但对于大堆来说,重定位是一个缓慢的过程。由于 ZGC 不希望有长时间的暂停,因此它在与应用程序并行的情况下完成大部分重定位工作。但这又引入了一个新问题。

假设我们有一个对对象的引用,ZGC 对其进行重定位,然后发生上下文切换,应用程序线程运行并尝试通过其旧地址访问该对象。ZGC 使用加载屏障来解决这个问题。加载屏障是一段代码,当线程从堆中加载引用时(例如,当我们访问对象的非基本字段时)运行。

在 ZGC 中,加载屏障检查引用的元数据位。根据这些位,ZGC 可能会在我们获取引用之前对其进行一些处理,因此,它可能会生成一个完全不同的引用,我们称之为 “重映射”。

3.2 标记

ZGC 将标记分为三个阶段。

第一阶段是全局停顿阶段。在此阶段,我们查找根引用并对其进行标记。根引用是访问堆中对象的起点,例如局部变量或静态字段。由于根引用的数量通常较少,因此此阶段很短。

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

最后一个阶段也是全局停顿阶段,用于处理一些边缘情况,例如弱引用。

此时,我们知道哪些对象是可访问的。

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

3.3 引用着色

引用表示虚拟内存中字节的位置。然而,我们不必使用引用的所有位来表示位置 —— 某些位可以表示引用的属性,这就是我们所说的 “引用着色”。

使用 32 位,我们可以寻址 4GB 内存。由于现在计算机的内存通常超过这个数量,我们显然不能使用这 32 位中的任何一位进行着色。因此,ZGC 使用 64 位引用,这意味着 ZGC 仅在 64 位平台上可用:

ZGC 引用使用 42 位来表示地址本身,因此,ZGC 引用可以寻址 4TB 的内存空间。除此之外,我们还有 4 位来存储引用状态:

  • finalizable 位:该对象仅可通过终结器访问
  • remap 位:引用是最新的,并指向对象的当前位置(请参见重定位)
  • marked0 和 marked1 位:用于标记可到达的对象

3.4 重定位

在 ZGC 中,重定位包括以下阶段:

  • 并发阶段:查找我们想要重定位的块,并将它们放入重定位集合中。
  • 全局停顿阶段:重定位重定位集合中的所有根引用,并更新它们的引用。
  • 并发阶段:重定位重定位集合中所有剩余的对象,并将旧地址和新地址之间的映射存储在转发表中。

剩余引用的重写发生在下一个标记阶段,这样我们就不必两次遍历对象树。或者,加载屏障也可以做到这一点。

在 JDK 16 之前,它通过使用堆预留来执行重定位。然而,从 JDK 16 开始,ZGC 获得了原地重定位的支持,这有助于避免在完全填满的堆上需要进行垃圾收集时出现 OutOfMemoryError 情况。

3.5 重映射和加载屏障

请注意,在重定位阶段,我们没有将大多数引用重写为重定位后的地址。因此,使用这些引用,我们将无法访问我们想要的对象,更糟糕的是,我们可能会访问垃圾。

ZGC 使用加载屏障来解决这个问题。加载屏障通过一种称为 “重映射” 的技术来修复指向重定位对象的引用。当应用程序加载引用时,它会触发加载屏障,然后加载屏障按照以下步骤返回正确的引用:

  1. 检查 remap 位是否设置为 1。如果是,则意味着引用是最新的,因此可以安全地返回它。
  2. 然后检查被引用的对象是否在重定位集合中。如果不在,则意味着我们不想对其进行重定位。为了避免下次加载此引用时进行此检查,我们将 remap 位设置为 1,并返回更新后的引用。
  3. 现在我们知道我们想要访问的对象是重定位的目标。唯一的问题是重定位是否发生过?如果对象已经被重定位,我们跳到下一步。否则,我们现在对其进行重定位,并在转发表中创建一个条目,该表存储每个重定位对象的新地址。之后,我们继续下一步。
  4. 现在我们知道对象已经被重定位,要么是由 ZGC 重定位的,要么是我们在前面的步骤中重定位的,或者是该对象在之前被访问时由加载屏障重定位的。我们将此引用更新为对象的新位置(要么使用前面步骤中的地址,要么通过在转发表中查找),设置 remap 位,并返回引用。

通过上述步骤,我们确保每次尝试访问对象时,都能获得指向它的最新引用。由于每次加载引用时都会触发加载屏障,因此它会降低应用程序性能,尤其是在第一次访问重定位对象时。但这是我们为了获得短暂停时间必须付出的代价。而且由于这些步骤相对较快,因此不会显著影响应用程序性能。

4. 如何启用 ZGC?

我们可以通过在命令行选项中使用 - XX:+UseZGC VM 标志为 JDK 15 及更高版本启用 ZGC:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
java -XX:+UseZGC <java_application>

5. 最新功能

5.1 NVRAM 上的堆

在过去十年中,NVRAM(非易失性 RAM)技术的进步使其速度更快、成本更低。因此,将整个 Java 堆放在 NVRAM 上对于许多工作负载来说是一种经济高效的选择。为 Java 堆空间分配指定备用内存设备路径:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
--XX:AllocateHeapAt=<path>

5.2 亚毫秒级最大暂停时间

ZGC 项目的目标之一是最小化垃圾收集(GC)暂停时间,最初目标是 10ms。这一目标最终在 JDK 16 中实现,其中 ZGC 现在具有低于 1ms 的 O (1) 暂停时间,并且不会随着堆或根集大小的增加而增加。

5.3 压缩类指针和类数据共享

压缩类指针功能通过压缩 HotSpot 中对象头的大小来减少堆使用量,允许类指针字段为 32 位而不是 64 位。以前,此功能还需要启用压缩普通对象指针(Compressed Oops),但在 JDK 15 中,打破了依赖关系,允许 ZGC 独立使用压缩类指针。

此外,类数据共享(Class Data Sharing)可以减少启动时间和内存占用,现在即使禁用了压缩普通对象指针功能,也可以与 ZGC 一起使用。

5.4 GC 线程数动态调整

JDK 的 - XX:+UseDynamicNumberOfGCThreads 选项使垃圾收集器能够根据工作负载和系统条件动态调整 GC 线程的数量。随着 JDK 17 中 ZGC 对该功能的支持,它优化了线程使用,以高效地收集垃圾,而不会消耗过多的 CPU,确保有更多的 CPU 时间可用于 Java 线程。

5.5 快速 JVM 终止

在某些情况下,使用 ZGC 终止 Java 进程可能需要一段时间,因为需要与垃圾收集器协调。然而,在 JDK 17 中,ZGC 进行了改进,通过中止正在进行的垃圾收集周期,按需快速达到安全状态。因此,终止运行 ZGC 的 JVM 现在几乎是即时的。

6. 结论

ZGC 旨在支持大堆并具有低应用程序暂停时间。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2025-06-12,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 大数据从业者 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 引言
  • 2. 主要概念
    • 2.1 内存管理
    • 2.2 多重映射
    • 2.3 重定位
    • 2.4 垃圾收集
    • 2.5 GC 阶段属性
  • 3. ZGC 概念
    • 3.1 总体概述
    • 3.2 标记
    • 3.3 引用着色
    • 3.4 重定位
    • 3.5 重映射和加载屏障
  • 4. 如何启用 ZGC?
  • 5. 最新功能
    • 5.1 NVRAM 上的堆
    • 5.2 亚毫秒级最大暂停时间
    • 5.3 压缩类指针和类数据共享
    • 5.4 GC 线程数动态调整
    • 5.5 快速 JVM 终止
  • 6. 结论
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档