ZGC 基础

581 阅读8分钟

ZGC是JDK 11中推出的一款低延迟垃圾回收器

JDK 11仅支持linux,JDK 14增加了对windows,mac OS的支持。在JDK 15 中正式转正。在JDK 21中,它被重新实现以支持分代。

从本质上讲,ZGC是一种并发垃圾回收器,这意味着所有繁重的工作都在Java线程继续执行的同时完成。这极大地限制了垃圾回收对应用程序响应时间的影响。

image.png

ZGC 设计理念与特点

设计目标

  • 停顿时间不超过10ms;

    JDK 16 发布,STW 时间缩短至 1ms 以内,并且时间复杂度为 O(1) image.png

  • 停顿时间不会随着堆的大小,或者活跃对象的大小而增加;

  • 支持8MB~4TB级别的堆。(JDK13支持16TB)。

    由于彩色指针的存在

    • 在 JDK 11~12 中,ZGC 默认使用 42 位来表示堆地址(即 2^42 = 4TB)。
    • JDK 13 开始扩展为使用 44 位堆地址(2^44 = 16TB)。

适用场景

  • 对响应时延比较敏感;
  • 机器的内存与 CPU 资源丰富;
  • 业务经过权衡后,认为 P99 的指标比 QPS 指标更重要。

性能瓶颈

  • 仅实现了单代内存管理,无法应对对象生命周期极短的场景;
    • 在 JDK 21支持分代 ZGC
  • 为了实现极低延迟,采用并发和增量回收,牺牲了一定的吞吐量,损失至多 15% 的吞吐率;

ZGC的特性

Concurrent 并发执行

在标记、转移和重定位阶段几乎都是并发的,这是停顿时间小于10ms的最关键原因

Region-based 基于Region

ZGC中的Region也被称为「ZPages」。ZPages被动态创建,动态销毁。与G1不同的是,G1的每个Region大小是完全一样的,而ZGC的Region大小分为3类:2MB,32MB,N×2MB,因此具有更好的灵活性。

image.png

  • 小区域:小区域的大小为2 MB。小于小区域八分之一(12.5%),即小于或等于256 KB的对象,存储在小区域中。

  • 中区域:中型区域的大小会根据最大堆大小(-Xmx)的设置而有所不同。如果最大堆大小为1GB或更大,中型区域的大小设置为32MB;如果最大堆大小低于128MB,则中型区域将被禁用。与小型区域一样,大小小于或等于中型区域设定大小八分之一(12.5%)的对象将存储在中型区域中。以下是中型区域大小范围的图表:

    Max Heap Size 最大堆大小Medium Region Size 中等区域大小
    >= 1024 MB32 MB
    >= 512 MB16 MB
    >= 256 MB8 MB
    >= 128 MB4 MB
    < 128 MBoff
  • 大区域:大区域预留给大型对象,并且以2MB为增量紧密适配对象的大小。因此,一个13MB的对象将存储在一个14MB的大区域中。任何太大而无法放入中型区域的对象都将被放置在其自己的大区域中。

Compaction and Relocation 压缩与重定位

Regions 的设计初衷是利用这样一个事实:大多数同时创建的对象也会同时离开作用域。然而,正如 “大多数” 这个限定词所暗示的,情况并非总是如此。通过内部垃圾回收启发式算法,垃圾回收器可能最终会将对象从一个主要由无法访问的对象填充的区域复制到一个新区域,以便可以释放旧区域并释放内存。这被称为压缩和重定位。自JDK 16起,ZGC通过就地和非就地两种重定位方法来完成压缩。

当存在空闲区域时,将执行非原地重定位,这是ZGC首选的重定位方法。以下是一个非原地重定位的示例:

image.png

如果没有可用的空闲区域,ZGC 将使用原地重定位。在这种情况下,ZGC 会将对象移动到一个占用率较低的区域。以下是原地重定位的一个示例:

image.png

使用原地重定位时,ZGC 必须首先将指定用于重定位对象的区域内的对象进行压缩。这可能会对性能产生负面影响,因为只有单个线程可以执行此工作。增加堆大小可以帮助 ZGC 避免使用原地重定位。

NUMA-aware 自动感知NUMA

NUMA(Non-Uniform Memory Access),即非均匀内存访问架构。是指多处理器系统中,内存的访问时间依赖于处理器和内存之间的相对位置。和处理器相对近的内存,通常被称作本地内存;和处理器相对远的内存, 通常被称为非本地内存。每个处理器优先访问本地内存,无需加锁访问内存总线,提高处理效率。

如何区分内存远近?

以我们目前线上的40核机器为例。它的主板上有2个CPU插槽,各插一个10核心的CPU,然后开启超线程,达到40核心。

其中这两个 CPU,各分一组地址总线,管理一部分自己的内存,其中他们使用自己管理的内存,速度较快。访问另一个核心的内存,速度较慢。这里 ZGC 可以优先在A核心优先处理A直管的内存。

image.png

colored pointers 彩色指针

image.png

不同于别的垃圾回收器把GC信息保存在对象头中,ZGC把GC信息保存在指针中。每个对象有一个64位指针(这也是ZGC只能用于64位操作系统的原因)。

当应用程序创建对象时,首先在堆空间申请一个虚拟地址,但该虚拟地址并不会映射到真正的物理地址。ZGC同时会为该对象在Marked0、Marked1和Remapped地址空间分别申请一个虚拟地址,且这三个虚拟地址对应同一个物理地址,但这三个空间在同一时间有且只有一个空间有效。

  • Marked0 / Marked1:这些位表明该对象是否已知被垃圾回收器(GC)标记。对于每个垃圾回收周期,ZGC会在这两个位之间交替,以确定哪一个是 “有效” 位。
  • Remapped:判断引用是否已指向新的地址
  • Finalizable:此位指示对象是否只能通过 finalizer 访问。请注意,在JDK 18中,随着 JEP 421:弃用终结机制以进行移除 的发布,终结机制已被标记为弃用以便移除。

每个位都有 “好” 颜色和 “坏” 颜色;然而,什么是 “好” 颜色或 “坏” 颜色,这将取决于访问对象时的上下文。应用程序本身不会感知到带颜色的指针;当从堆内存加载对象时,带颜色指针的读取由 加载屏障 处理。

Heap Multi-Mapping 堆多重映射

由于ZGC可以在应用程序运行时移动堆内存中对象的物理位置,因此需要为对象当前所在的物理位置提供多条路径。在ZGC中,这是通过堆多重映射来实现的。通过多重映射,对象的物理位置被映射到虚拟内存中的三个视图,分别对应指针可能的每种“颜色”。这样,如果对象自上次同步点以来发生了移动,加载屏障就可以定位到该对象。

这种设计决策的一个结果是,系统报告的ZGC内存使用量可能高于其实际使用量。这是由于对象在虚拟内存中有三重地址映射;然而,实际内存使用量仅来自实际对象所在的位置。当系统报告的内存使用量高于系统上安装的物理内存时,这一点最容易理解。下面的图表展示了多重映射在实际中的情况:

image.png

load barriers 加载屏障

加载屏障是由即时编译器(JIT)的C2编译器在JVM解析类文件时插入到类文件中的代码段。

在类文件中,加载屏障会被添加到从堆中获取对象的地方。

Object o = obj.fieldA();

<load barrier added here by C2> 

// o 是一个局部变量,指向一个已经加载并修正的对象。不是从堆中加载引用,只是堆栈中复制引用。
Object p = o; //No barrier, not a load from the heap

// 同样地,o 是局部变量,引用已经修正。
o.doSomething(); //No barrier, not a load from the heap

// 加载基本类型不涉及对象引用。
int i = obj.fieldB(); //No barrier, not and object reference

Object o = obj.fieldA();

  • 这是从堆中读取一个对象引用(fieldA 是引用类型)。
  • ZGC 会在这里插入一个 加载屏障

加载屏障的工作流程简化如下:

  1. 读取 obj.fieldA 指向的引用地址 ref
  2. 检查 ref 是否是已转发的对象(被 GC 移动过)
  3. 如果是,ZGC 会通过转发表或标记修正引用
  4. 最终返回有效的、更新后的对象地址

加载屏障增加了一种行为,当从堆中加载对象指针时,它会检查该指针的“颜色”。加载屏障针对“好”颜色的情况进行了优化,这是常见的情况,以便能够更快地通过。假设加载屏障遇到“坏”颜色。在这种情况下,它将尝试修复颜色,这可能意味着更新指针,将对象的新位置放在堆上,甚至在将引用返回给系统之前重新定位对象本身。这种修复确保了后续从堆中加载该对象时可以走快速路径。

学习文档

dev.java/learn/jvm/t…