关于Java中的Z Garbage Collector(ZGC)的文章:
- 垃圾收集是自动清理不再使用的对象以释放内存的过程。
- ZGC因其极短的暂停时间而闻名,设计目标是即使在处理大量内存时也保持暂停时间仅几毫秒。
Hotspot是JVM的一个流行实现,最初由Sun Microsystems开发,后由Oracle继续开发,并整合了JRockit平台的特性。OpenJ9是另一个重要的JVM实现,起源于IBM,目前由Eclipse Foundation维护。
Hotspot中的不同垃圾收集器
- Serial GC:适用于小应用和嵌入式系统,单线程收集器。
- Parallel GC(吞吐量收集器):适用于重视吞吐量、可容忍较长暂停时间的应用,使用多线程进行垃圾收集。
- G1 GC:从JDK 9开始成为默认垃圾收集器,平衡了高吞吐量和低延迟,能够设置可预测的暂停时间。
- ZGC:最先进的垃圾收集技术,专为要求极低暂停时间的应用设计,能够处理大量堆内存。
ZGC简介
- ZGC 是Java虚拟机(JVM)中的一种垃圾收集器(GC),专为需要极低暂停时间且能处理大量内存(高达数TB)的应用而设计。
- ZGC通过大部分与应用程序并发执行的垃圾回收工作,实现了仅几毫秒的暂停时间。
ZGC的特点
- 并发:ZGC在应用程序运行的同时执行大部分工作,最小化了暂停时间。
- 恒定暂停时间:无论堆大小或对象数量如何,ZGC都保持一致的暂停时间。
- 并行:ZGC使用多个线程并行处理垃圾收集,适应多核处理器的现代硬件。
- 压缩:ZGC在运行时压缩内存,防止长期内存碎片化。
- 基于区域:ZGC将内存分割成区域,集中处理垃圾较多的区域。
- NUMA感知:在NUMA架构的系统上,ZGC确保内存分配靠近需要它的CPU。
- 自动调优:ZGC根据工作负载自动调整,无需手动干预。
- 加载屏障和彩色指针:ZGC使用的技术手段,允许在应用程序运行时处理垃圾收集。
ZGC的生命周期包括三个主要阶段,大部分时间内应用程序继续运行:
- 第一阶段 - 标记开始:有一个非常短暂的暂停,在此期间 ZGC 会快速拍摄程序状态的快照。可以将其想象为快速拍摄程序中所有活动对象的照片。在此暂停期间,ZGC 会确定它将从哪里开始搜索活动对象(根)。在此短暂的暂停之后,您的程序将继续运行,同时 ZGC 会进行标记工作
- 并发标记阶段:在初始暂停之后,ZGC 从根开始并跟踪所有连接以查找仍在使用的对象。在此期间,ZGC 从根开始并跟踪所有连接以查找仍在使用的对象。这与程序的执行同时发生,这就是“并发”的含义。
- 标记结束阶段: ZGC 会再一次暂停,完成标记工作。类似于再次检查是否遗漏了重要信息。
- 准备重新定位阶段:这是一个并发阶段,ZGC 计划如何重新组织内存。这就像在人们仍在使用房间时计划如何重新布置家具一样。在此阶段,ZGC 决定需要移动哪些对象以及将它们移到哪里。
- 最后阶段 - 重新定位:再暂停后,ZGC 开始重新定位阶段。在此阶段,它实际上将对象移动到内存中的更好位置,使其更有条理、更高效。同样,这发生在程序继续运行时。
传统的垃圾收集器在执行大部分工作时需要完全停止您的程序。但 ZGC 只需要这三个极短的暂停,每次暂停通常不到一毫秒。其余工作同时进行 - 这意味着您的程序在 ZGC 进行清理工作时继续运行。
ZGC中的并发处理:
GC 的并发性在很大程度上取决于两个关键的架构特性:彩色指针 (Colored Pointers)和负载屏障 (Load Barriers)。
- 彩色指针:这些指针可帮助 ZGC 跟踪正在移动的对象,而无需停止程序
- 加载屏障:这些屏障可确保您的程序即使将对象移动到新位置仍能找到它们
1、彩色指针
彩色指针是 ZGC 架构的基本组成部分。与仅存储内存地址的传统指针不同,ZGC 使用 64 位指针,其中嵌入了额外的元数据。它的工作原理如下:
- 指针结构:在 64 位中,ZGC 保留 22 位用于元数据或“颜色”。其余位存储实际内存地址。这些额外信息允许 ZGC 直接在指针内跟踪对象的状态,从而无需单独的跟踪结构。
- 颜色代表什么?:这些指针的颜色表示对象的状态或状况。
- 标记位(元):这些显示对象是否已被垃圾收集器标记为活动。启用无需 STW 暂停的并发标记。
- 重新映射位(Remap):如果对象已被移动,这表示指针是否已更新到新的内存位置。
- 可终结位(Final):标记需要终结的对象。用于正确清理资源。
彩色指针背后的想法是允许垃圾收集器在应用程序运行时移动和管理对象而不会受到任何干扰。每当 GC 需要移动对象时,它都会更新指针中的颜色以表示对象已更改状态或位置。应用程序不必暂停,因为指针已经包含所有必要的信息。
这为什么有用?
通过将状态信息直接嵌入到指针中,ZGC 可以快速检查对象的状态并决定下一步要做什么,而无需暂停应用程序。这是 ZGC 将暂停时间缩短至几毫秒的关键部分,从而使垃圾收集过程更加高效。
2、负载屏障
虽然彩色指针提供了并发处理所需的状态信息,但加载屏障可确保每次访问对象时都能正确使用此信息。
负载屏障本质上是一小段代码,每当 JVM 访问堆上的对象时,它就会插入到应用程序的代码中。它的作用类似于检查点或过滤器。可以将其视为守门人:每次应用程序尝试使用对象时,它都会首先通过此检查点以确保一切正常。
当应用程序访问某个对象时:
- 加载屏障检查与该对象关联的指针的“颜色”。
- 如果颜色表明对象处于稳定状态(例如,最近没有移动),则加载屏障会让应用程序立即继续运行。
- 如果颜色表明对象处于过渡状态(例如,被垃圾收集器重新定位),则加载屏障会采取行动。它可能会将指针更新到对象的新位置,甚至在允许访问之前移动对象本身。
为什么负载屏障必不可少?
加载屏障至关重要,因为它们可以让应用程序和垃圾收集器保持同步,而无需暂停应用程序。它们使 ZGC 能够动态修复或调整指针,确保应用程序始终访问对象的正确版本,即使在垃圾收集期间移动对象也是如此。
通过检查对象的状态并在必要时“修复”,负载屏障可确保每个对象的访问都是安全和准确的。
这些过程发生得非常快,应用程序几乎看不到,这有助于 ZGC 保持较低的暂停时间。
ZGC 的前身:非分代模式
在JDK 21中引入分代模式之前,Z 垃圾收集器 (ZGC)使用的是所谓的非分代模式。简单来说,ZGC 对所有对象都一视同仁,无论它们在内存中存在多长时间。无论对象是刚刚创建的还是已经存在了一段时间,ZGC 在决定清理什么时都不会区分它们。这种方法可以很好地将暂停时间保持在很短的水平,但它并不是最有效的内存管理方法,尤其是在处理短暂存在的对象时。
ZGC中的分代模式:
现在,我们来谈谈分代模式。这个概念来自于这样的想法:程序中的大多数对象要么寿命短,要么寿命长:
- 短命对象:考虑在方法中创建的字符串或数字等临时数据,当该方法结束时它们会被遗忘。这些对象只能存活很短的时间。
- 长寿命对象:某些对象(例如数据库连接或缓存数据)往往会在整个程序运行期间存在。
在分代模式中,垃圾收集器将内存分成两个主要部分:
- 年轻代:这是存储新的、短暂对象的地方。由于许多对象都是短暂的,因此它们将被快速且频繁地收集。
- 老生代:在年轻代中存活足够长的对象被移到这里。这些对象存活时间更长,收集频率更低,这意味着垃圾收集器检查它们的暂停次数更少。
默认情况下,除非指定其他选项,否则 Java 将使用 G1 垃圾收集器。但是,如果您选择使用标志 启用 Z 垃圾收集器 (ZGC) 而不是默认的 G1 垃圾收集器-XX:+UseZGC,则从 JDK 23(带有 JEP 474)开始,ZGC 将自动以代际模式运行。
虽然非分代版本的 ZGC 仍受支持,但未来可能会被弃用。如果您希望继续使用非分代 ZGC,可以通过添加标志 -XX:+UseZGC ZGC的非分代模式和分代模式
文章还提供了一个使用Spring Boot应用比较ZGC和G1 GC性能的实践项目。
结论
- ZGC在保持极低暂停时间方面表现出色,对于需要极低延迟的应用来说是理想的选择。
案例研究
1、Netflix 采用 ZGC
最近,Netflix 进行了一项重大基础设施变革,将其流媒体服务从 JDK 21 上的 G1 GC 过渡到 Generational ZGC。此次迁移影响了其一半以上的关键基础设施,成为十年来最有影响力的运营改进之一。
在迁移之前,Netflix 一直苦苦应对由其 GRPC 和 DGS Framework 服务中的 GC 暂停导致的高尾部延迟问题。这些暂停导致请求取消,从而触发重试机制,造成一系列性能问题。他们之前使用非分代 ZGC 的尝试显示 CPU 利用率增加了 36%,这令人担忧,这让他们最初对过渡犹豫不决。
迁移结果在所有指标上均超出预期。GC 暂停时间降至亚毫秒级,同时提高了 CPU 利用率。新系统的性能比非分代 ZGC 提高了 10%,并且提供了更一致的内存可用性。运营优势同样令人印象深刻,系统只需进行极少的调整,并且无需进行数组池缓解。固定的 3% 堆大小开销被证明是可控的,并且系统比其前身更有效地处理大量数据刷新。
然而,迁移表明,某些工作负载类型在使用替代收集器时仍然表现更好。以吞吐量为导向的应用程序、分配率急剧上升的工作负载以及具有不可预测的对象保留模式的长时间运行的任务有时在使用 G1 或并行 GC 时会表现出更好的效果。
此次迁移的关键见解是,预期的性能权衡并未实现。Netflix 没有牺牲 CPU 效率来获得更好的暂停时间,而是在两个方面都取得了改进。事实证明,默认配置足以满足大多数服务的需求,而正确实施透明大页面则显著提高了性能。
2、HaloDoc 采用 ZGC:
印度尼西亚领先的医疗保健平台 Halodoc 最近实施了一项重大的性能优化计划,将其 Java 应用程序从 G1GC 过渡到 ZGC。作为一个为数百万用户提供关键医疗保健服务的平台,Halodoc 现有的 G1GC 实施面临着越来越多的挑战,特别是在高峰使用期间,他们的微服务会遇到高 CPU 开销和内存管理问题。
该公司的工程团队发现,G1GC 的被动内存管理方法导致资源利用率低下,尤其是在工作负载波动的服务中。这促使他们在 60 个微服务的基础架构中实施了更先进的垃圾收集器 ZGC。他们通过自定义优化增强了实施,包括 ZGenerational 垃圾收集以更好地处理短期对象和软限制参数以防止过度使用内存。
迁移过程通过金丝雀部署有序执行,使团队能够实时监控和微调性能。Halodoc 的工程团队能够显著提高系统性能。结果令人印象深刻:平均响应时间减少了 20%,内存使用量减少了 25%,系统吞吐量增加了 30%。也许最重要的是,垃圾收集时间减少了 10%,从而提高了应用程序性能。