ZGC
ZGC和G1,CMS一样都是一种垃圾回收器。那其实G1已经很不错了
为什么还需要ZGC呢
ZGC(The Z Garbage Collector)是JDK 11中推出的一款低延迟垃圾回收器,它的设计目标包括:
-
停顿时间不超过10ms;
-
停顿时间不会随着堆的大小,或者活跃对象的大小而增加;
-
支持8MB~4TB级别的堆(未来支持16TB)
先看下G1垃圾回收器
这里全红的即为需要STW的
ZGC的流程
GC 的完整工作流程如下:
-
初始标记(Initial Mark)
-
目标:标记 GC Roots 直接关联的对象。
-
操作:暂停所有应用线程(STW),快速标记根对象。
-
耗时:通常 <1ms。
-
-
并发标记(Concurrent Mark)
-
目标:遍历整个对象图,标记所有存活对象。
-
操作:GC 线程与应用线程并发执行,通过染色指针记录对象状态。
-
关键机制:
-
对象重定位(Relocation):标记过程中发现需要移动的对象,提前记录迁移信息。
-
再标记(Remark):处理并发标记期间发生变化的对象引用。
-
-
-
并发转移(Concurrent Transfer)
-
目标:将存活对象移动到新的内存区域,释放旧内存。
-
操作:
-
预转移(Pre-Transfer):为对象迁移做准备,更新元数据。
-
转移(Transfer):GC 线程与应用线程并发迁移对象,确保业务线程无感知。
-
-
关键技术:
-
读屏障(Load Barriers):在对象访问时自动修正指针,指向新内存地址。
-
自愈(Self-Healing):若迁移过程中出现指针失效,可通过染色指针快速恢复。
-
-
-
最终清理(Final Cleanup)
-
目标:回收完全空闲的内存页,重置 GC 周期。
-
操作:短暂 STW,清理元数据和统计信息。
-
ZGC关键技术
通过 并发标记 + 并发转移 + 实时地址修正,实现零停顿。
ZGC 通过 着色指针(Colored Pointer) 和 读屏障(Load Barriers) 技术,在对象转移过程中实现 无锁并发访问
着色指针(Colored Pointer)
Colored Pointer,染色指针是一种让指针存储额外信息的技术。我们知道在64位操作系统里,一个内存的地址总共64位,但是受限于实际物理内存的大小,我们其实并不是真正的使用所有64位。这里如果小伙伴了解linux的虚拟内存管理会很好理解,我这里大概解释一下。我们平时所说操作系统的“物理内存地址“并不是真正的”物理内存地址”,也就是说,并不是物理上,内存颗粒对应的地址。而是操作系统为我们虚拟的一个“虚拟地址”,这个技术被称为虚拟内存管理。虚拟内存基本在所有的linux服务器上都有使用,除了少部分嵌入式设备,因为内存太小不需要使用这种技术。在虚拟内存的帮助下,我们可以做到两个虚拟内存地址对应一个真实的物理地址。
对于JVM来说,一个对象的地址只使用前42位,而第43-46位用来存储额外的信息,即GC对象处于ZGC那个阶段。只使用46位的客观原因是linux系统只支持46位的物理地址空间,即64T的内存,如果一定想要使用更大的内存,需要linux额外的设置。但是这个内存设置在主流的服务器上都够用了。
在引用地址的划分上,对象引用第43位表示marked0标记,44位marked1标记,45位remapped标记,46位finalizable标记。指针染色就是给对应的位置为1,当然这三个位同一个时间只能有一个位生效。这些标记分别表示对象处于GC的那个阶段里。在下面ZGC的详细过程里我们会介绍染色指针怎么帮助GC的。指针的引用地址在各个标记之间切换也被称为指针的自愈。
-
Java Heap 的 4TB 是物理地址范围:
-
Java Heap 的 4TB 由 0~41 位直接映射的物理地址决定,与 M0/M1 的虚拟地址空间无关。
-
M0/M1 的虚拟地址空间通过高位元数据扩展,但物理内存仍受限于硬件和操作系统。
-
5. 实际地址转换示例
假设物理内存地址为 0xDEADBEEF
(42 位足够表示):
-
在 M0 中的虚拟地址:
高位元数据(0000) | 物理地址(0xDEADBEEF) => 0x0000DEADBEEF...
-
在 M1 中的虚拟地址:
高位元数据(0001) | 物理地址(0xDEADBEEF) => 0x0001DEADBEEF...
-
切换地址空间:
-
通过原子操作将默认元数据从
0000
改为0001
,所有新访问自动重定向到 M1。
-
核心设计:
-
高位存储元数据: ZGC 将 64 位指针的高 4 位(第 42~45 位)用作元数据标记,而非传统对象头存储存活状态。
位范围 用途 42~45 (4 bits) 对象存活状态(标记、转移阶段等) 0~41 实际物理地址 -
标记位含义:
-
-
虚拟地址空间划分: ZGC 将 64 位地址空间划分为多个子空间,同一物理内存对应三个虚拟地址:
虚拟地址空间 | 地址范围 | 作用 |
M0 | [4TB, 8TB) | 存放当前活跃对象的最新地址 |
M1 | [8TB, 12TB) | 存放对象转移后的新地址 |
Remapped | [16TB, 20TB) | 用于地址重定向(空间换时间) |
ZGC中读屏障的代码作用:
在对象标记和转移过程中,用于确定对象的引用地址是否满足条件,并作出相应动作。
接下来详细介绍ZGC一次垃圾回收周期中地址视图的切换过程:
-
初始化:ZGC初始化之后,整个内存空间的地址视图被设置为Remapped。程序正常运行,在内存中分配对象,满足一定条件后垃圾回收启动,此时进入标记阶段。
-
并发标记阶段:第一次进入标记阶段时视图为M0,如果对象被GC标记线程或者应用线程访问过,那么就将对象的地址视图从Remapped调整为M0。所以,在标记阶段结束之后,对象的地址要么是M0视图,要么是Remapped。如果对象的地址是M0视图,那么说明对象是活跃的;如果对象的地址是Remapped视图,说明对象是不活跃的。
-
重新标记(Remark)
这个阶段是处理一些并发标记阶段未处理完的任务(少量STW,控制在1ms内)如果没处理完还会再次并发标 记,这里其实主要是解决三色标计算法中的漏标的问题,即白色对象被黑色对象持有的问题。并发标记阶段发 生 引用更改的对象会被记录下来(触发读屏障就会记录),在这个阶段标记引用被更改的对象
-
并发预备重分配(Concurrent Prepare for Relocate)
这一步主要是为了之后的迁移做准备,这一步主要是处理软引用,弱引用,虚引用对象,以及重置page的Forwarding table, 收集待回收的page信息到Relocation Set
Forwarding table是记录对象被迁移后的新旧引用的映射表。Relocation Set是存放记录需要回收的存活页集合。这个阶段ZGC会扫描所有的page,将需要迁移的page信息存储到Relocation Set,这一点和G1很不一样,G1是只选择部分需要回收的Region。在记录page信息的同时还会初始化page的Forwarding table,记录下每个page里有哪些需要迁移的对象。这一步耗时很长,因为是全量扫描所有的page,但是因为是和用户线程并发运行的,所以并不会STW,而且对比G1,还省去了维护RSet和SATB的成本。
-
初始迁移(Relocate Start)
这个阶段是为了找出所有GC Roots直接可达的对象,并且切换good mask到remapped,这一步是STW的。这里注意一个问题,被GC Roots直接引用的对象可能需要迁移。如果需要,则会将该对象复制到新的page里,并且修正GC Roots指向本对象的指针,这个过程就是“指针的自愈”。当然这不是重点重点是切换good mask.
-
并发转移阶段:标记结束后就进入转移阶段,此时地址视图再次被设置为Remapped。如果对象被GC转移线程或者应用线程访问过,那么就将对象的地址视图从M0调整为Remapped。
其实,在标记阶段存在两个地址视图M0和M1,上面的过程显示只用了一个地址视图。之所以设计成两个,是为了区别前一次标记和当前标记。也即,第二次进入并发标记阶段后,地址视图调整为M1,而非M0。
着色指针和读屏障技术不仅应用在并发转移阶段,还应用在并发标记阶段:将对象设置为已标记,传统的垃圾回收器需要进行一次内存访问,并将对象存活信息放在对象头中;而在ZGC中,只需要设置指针地址的第42~45位即可,并且因为是寄存器访问,所以速度比访问内存更快
ZGC的优势很明显,几乎全程并发的回收过程带来了无与伦比的低暂停时间,这也是ZGC的设计思路。低暂停时间加上JAVA本身的支持高并发的特点,假以时日ZGC将来一定是能在服务器领域的展现它大杀器级别的威力。但是为了达到这个设计目标,ZGC其实也牺牲了一些东西,比如吞吐量以及在JDK21之前ZGC不分代
调优案例(参考美团技术文章)
案例一:秒杀活动中流量突增,出现性能毛刺
(1)开启”基于固定时间间隔“的GC触发机制:-XX:ZCollectionInterval。比如调整为5秒,甚至更短。
(2)增大修正系数-XX:ZAllocationSpikeTolerance,更早触发GC。ZGC采用正态分布模型预测内存分配速率,模型修正系数ZAllocationSpikeTolerance默认值为2,值越大,越早的触发GC,Zeus中所有集群设置的是5。
案例二:压测时,流量逐渐增大到一定程度后,出现性能毛刺
增大-XX:ConcGCThreads, 加快并发标记和回收速度。ConcGCThreads默认值是核数的1/8,8核机器,默认值是1。该参数影响系统吞吐,如果GC间隔时间大于GC周期,不建议调整该参数。
案例三: 单次GC停顿时间30ms,与预期停顿10ms左右有较大差距
升级Aviator组件版本,避免生成多余的ClassLoader。
案例四:服务启动后,运行时间越长,单次GC时间越长,重启后恢复
通过业务优化解决,删除不需要执行的Aviator表达式,从而避免了大量Aviator方法进入CodeCache中。