JDK21在用,目前最新的垃圾回收器------ZGC垃圾回收器原理简析
欢迎关注,分享更多原创技术内容~
微信公众号:ByteRaccoon、知乎\稀土掘金都叫:浣熊say
微信公众号海量Java、数字孪生、工业互联网电子书免费送~
ZGC简述
什么是ZGC?
ZGC(Z Garbage Collector)是Java虚拟机中的一种垃圾回收器,属于低延迟垃圾回收器,它的设计目标就是在保持较低的停顿时间的同时,能够处理非常大的堆内存。
目前已经可以将停顿时间降低到10ms左右。ZGC是从JDK 11版本开始引入的,并在后续版本中进行了改进和优化。
如上图所示是ZGC垃圾回收器和Parallel和G1等垃圾回收器相比较起来的GC Pause,可以发现其GC 停顿已经降低到的很客观的程度,并且这个GC停顿的时间还是可以自主进行配置的。
ZGC的实现没有采用分代收集机制,但是和G1垃圾回收器类似,采用了分区策略,下面是ZGC的一些特性和设计原则:
- 低停顿时间: ZGC的主要设计目标之一是实现低停顿时间,停顿时间(STW)不会超过10ms,这个停顿时间还可以根据实际的项目需求进行配置;
- 处理大内存堆: ZGC被设计为能够处理非常大的堆内存,支持8MB~4TB级别的堆内存,未来会支持16TB;
- 并发收集: ZGC在执行垃圾回收时,会与应用程序线程并发工作,减少了停顿时间,这意味着即使在进行垃圾回收时,应用程序也能够继续执行,降低了对应用性能的影响;
- 可预测的停顿: ZGC追求更加可预测的停顿时间,避免因垃圾回收而引起的不确定性。这对于需要保证应用程序的响应时间和稳定性的场景非常重要;
- 处理不同的内存分配模式: ZGC适应了现代应用程序中常见的内存分配模式,包括大对象、小对象、短期存活对象等,以提高垃圾回收的效率;
- 实时可达性分析: ZGC使用了一种实时的可达性分析算法,以更快地找到不再使用的对象,减少垃圾回收的时间。
总体而言,ZGC是为了满足对低延迟和大内存的需求而设计的一种垃圾回收器,适用于对应用程序响应时间要求较高的场景。
ZGC启用和相关参数
在Java应用程序中启用ZGC,可以使用以下命令行参数:
bash
java -XX:+UnlockExperimentalVMOptions -XX:+UseZGC -jar YourApplication.jar
这个命令包括两个关键的VM选项:
-XX:+UnlockExperimentalVMOptions
:解锁实验性的VM选项,允许使用实验性的垃圾回收器。-XX:+UseZGC
:启用Z Garbage Collector。
ZGC是作为实验性特性引入的,具体取决于您使用的JVM版本,需要确保你的Java版本支持ZGC,另外一些ZGC的常见重要参数如下:
参数 | 描述 | 默认值 |
---|---|---|
-XX:ZCollectionInterval | 固定时间间隔进行 GC | 0 |
-XX:ZAllocationSpikeTolerance | 内存分配速率预估的一个修正因子 | 2 |
-XX:ZProactive | 是否启用主动回收策略 | true |
-XX:ZUncommit | 将不再使用的内存还给 OS(JDK13及以上版本) | - |
-XX:+UseLargePages -XX:ZPath | 使用大内存页,配置 Huge Pages 可以提高性能 | - |
-XX:UseNUMA | 启用 NUMA 支持 | true |
-XX:ZFragmentationLimit | 根据当前 region 已大于 ZFragmentationLimit,则回收 | 25 |
-XX:ZStatisticsInterval | 设置打印 ZStat 统计数据的间隔(CPU、内存等日志) | - |
ZGC(Z Garbage Collector)原理
ZGC内存布局
ZGC(Z Garbage Collector)的内存布局与G1(Garbage-First)在内存布局上有些相似,都采用了基于Region的堆内存布局。ZGC在内存布局方面引入了一些动态性的概念,Region的大小不是固定不变的,也不会区分新生代、老年代区域(实际在JDK21中又引入了新老年代分区的概念,后续我会继续跟踪出一版新的原理),使得它更加灵活和适应不同场景。
ZGC的内存布局主要涉及到动态创建、动态销毁以及动态的区域容量大小,以下是ZGC内存布局的一些关键特点:
- 动态创建: ZGC的Region可以根据需要动态创建,系统可以根据应用程序的实际内存需求,动态生成新的内存区域,以适应不同的工作负载。
- 动态销毁: 类似地,ZGC的Region也可以根据垃圾回收的情况动态销毁,当某个区域内的对象被回收后,该区域可以被标记为可用,随后可以被重新分配或销毁,以便更好地管理内存。
- 动态的区域容量大小: ZGC引入了不同大小的Region,包括Small Region(2MB)、Medium Region(32MB)和Large Region(可变大小),使得ZGC在内存分配时能够更好地适应不同大小的对象,提高内存利用率。
如上图所示,ZGC的Region分类包括:
-
Small Region(小区域): 大小为2MB,用于存放小于256KB的小对象。这有助于提高小对象的分配和回收效率。
-
Medium Region(中等区域): 大小为32MB,用于存放大于等于256KB但小于4MB的对象。中等区域的引入有助于处理中等大小的对象。
-
Large Region(大区域): 大区域的大小是可变的,最小为4MB。每个大区域只用于存放一个大对象,且不会被重新分配,这有助于处理大对象,减少内存碎片化。
这种动态的内存布局设计使得ZGC能够更好地适应不同的工作负载和对象分布模式。它能够在提供低延迟的同时,更加高效地管理内存空间,降低内存碎片化的程度。
ZGC的垃圾标记算法------染色指针
什么是染色指针?
在ZGC出现之前,GC信息被保存在对象头的Mark Word当中,如64位的JVM,对象头的Mark Word中保存的信息如下:
64位的MarkWord中保存了GC和对象相关的信息,包括:对象的哈希码、分代年龄、锁记录等。由于这些信息存在于对象头的MarkWord中,而对象实体一般存储在堆内存中,虚拟机栈帧中的局部变量表中存储的则是堆中对象的引用(指针)。因此,当访问对象的时候获取这些信息是很方便的,但是当我们在希望不直接访问对象,仅仅通过对象的指针就得到这些信息的话,这种实现方式就做不到。
但是,在追踪式垃圾回收算法的标记阶段,例如:三色标记算法,则可能存在只需要处理指针而无需加载指针所引用的对象的情况。例如在三色标记算法当中,JVM需要给对象打上黑、白、灰色的标记,但是这些标记只与对象的引用有关,与对象本身的其它属性无关。这样的场景下理论上我们只需要为对象的指针打上标记即可,无需在堆内存中为实际的对象打上标记。
然而,当前HotSpot虚拟机对标记方案的实现各不相同,采用了包括:对象头标记、BitMap映射和染色指针等不同方法,具体如下:
- Serial、PS、ParNew、CMS:这些垃圾回收器都是直接将标记信息打在对象头上的,因此写入和读取标记需要时机访问堆中对象。
- G1、Shenandoah:将标记信息记录在与对象独立的数据结构上,通常是一种BitMap的结构,一般相当于堆内存的1/64大小。
- ZGC:采用染色指针的方式,直接将标记信息存储在引用对象的指针上,从而实现遍历"引用图"来标记"引用"的效果,而不是遍历对象图来标记对象。
总之,理解染色指针首先要理解三色标记算法这个垃圾标记算法,三色标记算法会为对象打上白、灰、黑三种颜色。在ZGC之前的垃圾回收器中,颜色标记都是被打在对象头的MarkWord当中的,这样判断是否是垃圾需要去实际访问堆中对象。
而染色指针则可以将颜色标记直接记录在指针上,这样就省去了访问实际对象的过程。这样一来,垃圾标记阶段遍历的不再是对象图来标记垃圾,而是通过遍历"引用图"来标记垃圾,引用的对象便是堆中的对象。
染色指针的实现原理
染色指针的结构
染色指针简单来说就是一种将额外少量的垃圾标记信息(颜色信息)存储在对象指针上的技术,在64 位操作系统中,对象指针的长度也是64位,染色指针的结构图如下:
在染色指针中,高18位都是0暂未使用,剩余的46位实际上是能支持64TB的内存的,但是目前来说计算机内存空间还没这么大。于是剩余的46位中,高4位用来保存了4个标志位,低42位置才是用来保存对象的指针,所以ZGC最大可以管理的内存不超过4TB。染色指针中4个标志位的具体作用如下:
- Marked 0和Marked 1标志位:
- 作用: 用于表示对象的三色标记状态,通常用于垃圾收集算法中的标记阶段。
- 意义: 提供了直观的对象垃圾收集状态,通过这两个标志位,JVM可以轻松地追踪对象的标记状态,即未标记、已标记(Marked 0或Marked 1)等。
- Remapped标志位:
- 作用: 表示对象是否已经进入了重分配集,即是否需要在内存重分配时进行特殊处理。
- 意义: 对于ZGC垃圾收集算法,该标志位的存在能够提供更高效的内存重分配,避免不必要的复制或移动操作,从而提高垃圾收集的性能。
- Finalizable标志位:
- 作用: 标识对象是否需要通过finalize方法进行访问,即是否需要执行清理和释放资源的操作。
- 意义: 允许JVM更灵活地处理对象的生命周期,根据Finalizable标志位的状态来判断是否触发finalize方法,这在一些需要资源释放的情景下尤为重要。
通过这四个标志位,JVM可以直接从对象的指针上获取关键的状态信息,而无需访问对象本身的其他属性,这种直接的标志位设计有助于提高垃圾收集的效率和性能。
虚拟内存映射
但是,上面的染色指针的实现方案中存在着一个问题------如何映射到真实的内存物理地址。因为JVM作为一个普通的进程,这样随意的定义指针中的某几位操作系统是不认可得。实际上Java程序最终都会被转换成机器指令交给具体的平台去执行,CPU可不会认可你Java自己定义的指针结构,只会把整个指针都看作普通的内存地址去映射到物理内存地址。一般的x86-64平台也不支持重新定义机器指令,因此,ZGC就只有采用了虚拟内存映射这个技术来解决这个问题。
在x86平台上,CPU使用分页管理机制将线性地址空间 和物理地址空间划分为大小相同的块,即"页"(Page)。通过建立映射表,分页管理机制完成线性地址到物理地址的转换。这保证了程序对虚拟内存的访问可以映射到相应的物理内存地址上。可以简单理解为使用mmap将不同的虚拟内存地址映射到同一个物理内存地址上。
如上图,ZGC采用虚拟内存映射技术,将同一块物理内存映射为Marked 0、Marked 1和Remapped这三个虚拟内存。每个对象在堆上申请虚拟地址时,ZGC为该对象在这三个视图空间分别分配虚拟地址,这三个虚拟地址映射到同一个物理地址。
染色指针中,Marked 0、Marked 1和Remapped作为ZGC的三个视图空间,在同一时间点内只能有一个是有效的。通过切换这三个视图空间,ZGC实现了并发的垃圾回收,对象的标记信息和状态可以在不同的视图之间切换,从而实现垃圾回收的并发性。在后续的ZGC回收流程中我们将详细介绍这三个视图空间的切换流程。
指自愈指针(Self-Healing Pointers)
自愈指针(Self-Healing Pointers)是指一种用于在并发垃圾回收中进行引用修复的技术。这种技术的目的是在对象移动时,通过修改指针本身而不是对象的引用关系,来维护正确的引用关系,从而避免了对引用对象的访问和修改。
在垃圾回收的过程中,如果对象发生移动,原本指向该对象的引用关系就会失效。传统的垃圾回收器需要遍历对象图,修复所有指向移动对象的引用,这会带来一定的性能开销。而自愈指针技术通过直接修改指针来维护引用关系,减少了对对象的访问和修改,提高了并发性能。
在自愈指针技术中,指针的高位(通常是一些特定的位或字节)被用来存储额外的信息,例如对象的新地址、标记信息等。当对象移动时,只需要修改指针的高位信息,而不需要访问对象本身,从而实现引用的自愈。
这种技术通常应用于并发垃圾回收算法中,其中对象的移动是允许的,并发标记和并发移动阶段需要通过自愈指针来保证引用的正确性。ZGC(Z Garbage Collector)就是一种使用了自愈指针技术的垃圾回收器,它通过虚拟内存映射和染色指针来实现并发垃圾回收,减小了对引用修复的停顿时间。
染色指针的优势
- 即时回收空间: 当某个Region中的存活对象被成功移走后,该Region就能够立即释放和重用,而无需等待整个堆中所有指向该Region的引用都被修正。这意味着,理论上只要还有一个空闲的Region,ZGC就能完成垃圾收集。相比之下,Shenandoah需要等到更新阶段结束才能释放回收集中的Region,特别是在Region内的对象都存活时,需要1:1的空间才能完成收集。
- 减少内存屏障使用: 染色指针可以显著减少在垃圾收集过程中内存屏障的使用数量。ZGC仅使用了读屏障,而不需要其他类型的内存屏障。这有助于提高垃圾收集的效率和性能。
- 强大的扩展性: 染色指针具备强大的扩展性,可以作为一种可扩展的存储结构,用于记录与对象标记、重定位过程相关的更多数据。这为将来进一步提高性能提供了可能性,使ZGC能够灵活适应不同的场景和需求。
ZGC的垃圾回收原理
ZGC触发时机
ZGC的触发时机主要取决于堆的占用情况以及对象分配的速率。ZGC是一种响应式的垃圾回收器,会在满足一定触发条件时启动垃圾回收。以下是ZGC触发的主要时机:
- **堆空间占用达到阈值:**当堆空间的占用达到一定的阈值时,ZGC可能会被触发。这个阈值可以通过启动JVM时的参数进行配置。一旦堆空间占用超过了指定的阈值,ZGC就有可能被启动。
- **对象分配速率:**ZGC会监测对象的分配速率。如果对象分配速率较高,导致堆空间迅速被占满,ZGC可能会被启动以回收内存。这种情况下,ZGC可以防止堆空间迅速耗尽,保持应用程序的正常运行。
- **空闲时间较长:**ZGC具有适应性的特性,会根据应用程序的执行情况和空闲时间来判断是否进行垃圾回收。当应用程序处于空闲状态时,ZGC可能会选择启动垃圾回收,以尽量减小对应用程序的干扰。
- **手动触发:**开发人员也可以通过Java Management Extensions(JMX)或其他工具手动触发ZGC的垃圾回收。这在一些特殊场景下可能会被使用,例如在某个业务逻辑执行完成后手动触发垃圾回收以及时释放内存。
ZGC是为了保持低停顿时间而设计的,因此它可能根据应用程序的需求动态调整触发时机,以最大程度地减小停顿时间。在大多数情况下,ZGC的触发时机是由垃圾收集器自动管理的。
ZGC垃圾回收整体流程
ZGC(Garbage Collector)是一种在JVM中实现的低停顿时间垃圾回收器。其垃圾回收流程可以概括为以下几个关键阶段:
-
**初始标记阶段(Initial Mark):**与G1垃圾回收器一样,初始标记阶段是一个短暂的STW阶段,目的是标记出根对象直接引用的对象,标记的过程是并发执行的,所以这个阶段的停顿时间很短。
-
**并发标记阶段(Concurrent Mark):**在这个阶段,ZGC并发地标记出所有可达的对象,包括从根对象出发的引用链上的对象,这个过程是与应用程序的执行同时进行的,因此对停顿时间的影响很小。
-
**再标记阶段(Remark):**如果在并发标记阶段有新的对象被创建或有对象被回收,ZGC可能需要进行一次短暂的STW再标记。这个阶段的停顿时间一般不超过1毫秒。在这个阶段,ZGC会修正并发标记阶段可能由于并发引起的标记不一致。
-
**并发转移准备(Concurrent Prepare for Relocate):**在这个阶段ZGC进行整堆扫描,确定收集哪些Region,并将这些Region组成重分配集(Relocation Set)。与G1收集器不同,ZGC的重分配集扫描所有的Region,而不是计算最有价值回收的Region,由于染色指针的存在,扫描过程会很快。这个过程并不是为了计算最优的回收集,而是为了确定存活对象将被复制到其他Region。此阶段还涉及到JDK12支持的类卸载和弱引用的处理。
-
**初始转移阶段(Initial Relocation):**这是ZGC垃圾回收的核心阶段之一,ZGC并发地将重分配集中的存活对象复制到新的Region。为了记录从旧对象到新对象的转移关系,ZGC需要为重分配集中的每个Region维护一个转发表(Forward Table)。
ZGC收集器能仅从引用上就明确得知一个对象是否处于重分配集之中,如果用户线程此时并发访问了位于重分配集中的对象,这次访问将会被预置的内存屏障所截获,然后立即根据Region上的转发表记录将访问转发到新复制的对象上,并同时修正更新该引用的值,使其直接指向新对象,ZGC将这种行为称为指针的"自愈"(Self-Healing)能力
ZGC的染色指针因为"自愈"(Self-Healing)能力,所以只有第一次访问旧对象会变慢,而Shenandoah的Brooks转发指针是每次都会变慢。 一旦重分配集中某个Region的存活对象都复制完毕后,这个Region就可以立即释放用于新对象的分配,但是转发表还得留着不能释放掉,因为可能还有访问在使用这个转发表。
-
并发转移阶段(Concurrent Relocation):并发转移阶段的工作就是修正堆中指向重分配集中旧对象的所有引用,也可以直接认为就是真正进行对象引用修复的一个步骤,从这一点来看shenandoah的并发引用更新阶段是一样的。但是ZGC并不需要马上完成这个操作(因为有指针自愈的特性),ZGC把并发重映射阶段要做的工作巧妙的合并到下一次垃圾收集循环 中的并发标记阶段中去完成,这样做的好处是节省遍历对象图的开销。一旦所有指针修复,新旧对象的引用关系转发表就可以释放了。
以上阶段中,除了初始标记、再标记阶段和初始转移阶段有短暂的STW时间外,其他阶段都是并发执行的,尽量减小了垃圾回收对应用程序的干扰,从而实现低停顿时间。这种并发执行的特性使得ZGC适用于大内存堆的Java应用,尤其是需要低延迟的场景。
ZGC垃圾回收并发处理的视图切换
单纯从ZGC的整体流程上看,似乎很难看出染色指针在ZGC的垃圾回收流程的作用,实际上ZGC在垃圾标记的过程中会改变染色指针的颜色位,将内存区域切换成ReMapped、M0 或者 M1状态,方便快速判断对象的存活状态。同时,由于扫描指针比较清亮,ZGC每次执行都是进行全堆扫描的。具体视图切换的地方如下图所示:
如上图所示,ZGC的垃圾回收周期中涉及的地址视图切换过程可以分为以下几个阶段:
- **初始化阶段:**初始时,整个内存空间的地址视图被设置为Remapped,意思是该内存的Region处于重置集当中,无需进行转移或者垃圾清理。程序正常运行和分配对象,并在一定条件下触发垃圾回收。
- **并发标记阶段:**第一次进入标记阶段时,视图为M0。如果对象被GC标记线程或应用线程访问过,将对象的地址视图从Remapped调整为M0。标记阶段结束后,对象的地址要么是M0视图,说明对象是活跃的;要么是Remapped视图,说明对象是不活跃的。
- **并发转移阶段:**标记结束后,进入转移阶段,此时地址视图再次被设置为Remapped。如果对象被GC转移线程或应用线程访问过,将对象的地址视图从M0调整为Remapped。
- **第二次并发标记阶段:**在第二次进入并发标记阶段时,地址视图调整为M1,而非M0。这样设计是为了区别前一次标记和当前标记,确保在不同的标记阶段使用不同的地址视图。
在并发标记阶段,采用着色指针 和读屏障技术。将对象设置为已标记时,只需设置指针地址的第42~45位,不需要进行一次内存访问,且速度比访问内存更快。这种技术使得传统的将对象存活信息放在对象头中的方式变得不再必要,从而提高了标记的效率。
通过这样的设计,ZGC实现了并发的垃圾回收,减少了停顿时间,并通过多次标记阶段和地址视图的切换,有效地管理对象的存活状态。
ZGC的优缺点
优点 | 描述 |
---|---|
1. 低停顿 | ZGC以低停顿为首要目标,几乎所有垃圾回收过程都是并发的,只有短暂的STW。 |
2. 高吞吐量 | ZGC在吞吐量方面取得了显著进展,超越了G1,接近Parallel Scavenge。 |
3. 内存小 | 没有写屏障和卡表等额外的数据结构,收集过程中额外耗费的内存较小。 |
4. 局部内存分配 | 在多核处理器的某些架构下,优先在当前线程所处的处理器的本地内存上分配对象。 |
5. 并发停顿 | 并发停顿非常短暂,大部分过程都是与应用线程并发执行。 |
6. 无分代 | 没有引入分代的概念,简化了内存管理的复杂性。 |
7. 无内存碎片 | 采用并发的标记-整理算法,没有内存碎片问题。 |
缺点 | 描述 |
---|---|
1. 浮动垃圾 | 承受的对象分配速率不会太高,产生浮动垃圾,难以及时回收。 |
2. 执行时间较长 | 停顿时间短,但整个垃圾回收过程的执行时间可能较长。 |
3. 无分代概念 | 没有分代概念,可能导致朝生夕死的对象无法及时回收。 |
4. 平台限制 | 目前仅在Linux/x64上可用,可能限制了在其他平台的应用。 |
总结
ZGC和G1垃圾回收器一样采用了Region的机制来完成对垃圾的回收和标记,不同的是ZGC将Region分为了small、medium和large,并且单个region可以存放多个对象,但是大对象只会存放在单个large区域当中。
对于垃圾的发现算法,ZGC采用了和以前的垃圾回收器都不同的染色指针算法,可以根据对象指针就能够快速的判断对象的存活状态,而无需去对象头中读取相关的信息,垃圾标记效率显著提升。
最终,垃圾的标记阶段和CMS、G1垃圾回收器有些类似,都有初始标记、并发标记、最终标记来尽可能多的标记好堆中的垃圾。之后,ZGC独有的并发转移准备、初始转移和并发转移会维护一个Region之间的转发表,来完成垃圾的高效回收。
本文只是对ZGC垃圾回收器概念上和基本原理的浅析,关注我,后续带大家从JDK 21源码理解ZGC的核心原理。(PS:我觉得网上的bolg多多少少有些问题,后续打算开个专栏从HotSpot源码解析来真正的出一份正确的JVM核心原理的专栏,这里挖个坑~)