前言
在Java开发中,垃圾收集器的选择对系统性能有着致命的影响。Java 8后,虽然G1 GC成为默认,但是它在延迟性控制上仍有限。ZGC作为最新一代高性能低延迟垃圾收集器,解决了CMS和G1在延迟、垃圾堆容量和吞吐量方面的重大突破。本文将完整给出ZGC的技术原理和实际应用,帮助您做出最适合应用场景的GC选型。
Java中的垃圾收集概述
垃圾收集的意义
Java采用自动内存管理(GC),帮助开发者自动处理不再使用的对象内存。GC的目标是在尽量减少延迟的同时,回收无用对象,维持系统的稳定运行和内存调度。
当前垃圾收集系统培胜于 "根集" 分析,根集为程序可达对象的集合,GC进行探游,找出可达对象,其余则可以处理成垃圾。
Java堆内存分区
Java堆内存通常分为两大区域:
Young Generation (年轻代):新生成的对象,较简单。内部分为Eden和Survivor S0/S1。
Old Generation (老年代):清除尽Young GC后仍然存活的对象,常应用于Full GC。
有些GC器同时使用总缓存(Metaspace)、堆外内存区域、选择性老年代等特殊区域。
Java垃圾收集器的演进
在深入了解ZGC之前,我们需要先回顾Java中垃圾收集器的发展历程,特别是ZGC推出之前在Java 8及更早版本中常见的几种GC方式。这不仅帮助我们理解ZGC为何而生,也为我们提供比较其优劣的视角。
Java 8中的主要垃圾收集器
Java 8中默认的垃圾收集器是Parallel GC,但在很多中大型项目中,开发者常常会根据不同的业务需求切换为CMS(Concurrent Mark-Sweep)或者G1(Garbage First)。下面我们来分别了解这些GC方式的基本特点与运行机制。
1. Serial GC
适用于单核处理器或者内存较小的客户端应用。
特点:串行执行,GC过程中STW(Stop-The-World)时间较长。
优点:实现简单,适用于内存小、线程少的环境。
缺点:不适合服务器端或多线程环境。
// 启用Serial GC
java -XX:+UseSerialGC -Xms512m -Xmx512m MyApp
2. Parallel GC(吞吐量优先GC)
也称为吞吐量GC,追求最大程度的吞吐量,适用于批处理和计算密集型任务。
特点:多个GC线程并行执行,仍会发生Stop-The-World。
优点:高吞吐、GC时间相对较短。
缺点:GC期间程序线程全部暂停。
// 启用Parallel GC
java -XX:+UseParallelGC -Xms1g -Xmx1g MyApp
3. CMS GC(并发标记清除)
目标是减少GC对程序运行的影响,引入并发阶段。
特点:多阶段GC流程,包括初始标记、并发标记、重新标记、并发清除。
优点:在标记和清除阶段大部分操作可并发,适合响应时间敏感型应用。
缺点:内存碎片化严重,标记过程复杂。
// 启用CMS GC
java -XX:+UseConcMarkSweepGC -Xms2g -Xmx2g MyApp
4. G1 GC(Garbage First)
G1是在Java 8中被引入并逐渐替代CMS的收集器,强调可预测的停顿时间。
特点:堆被分成多个小区域(Region),混合收集老年代和年轻代。
优点:减少Full GC频率,支持大内存,延迟控制能力较CMS强。
缺点:在低延迟场景仍有不可预测的长时间停顿。
// 启用G1 GC
java -XX:+UseG1GC -Xms4g -Xmx4g MyApp
各GC对比总结
GC 类型 | 并发能力 | 停顿时间 | 吞吐量 | 内存使用效率 | 是否碎片整理 |
---|---|---|---|---|---|
Serial | 无 | 高 | 中 | 高 | 是 |
Parallel | 否 | 中等 | 高 | 中 | 是 |
CMS | 是 | 低 | 中 | 低(有碎片) | 否 |
G1 | 是 | 中到低 | 中 | 高 | 是 |
从上表中可以看出,虽然CMS和G1 GC在延迟方面取得了一定进展,但仍存在以下痛点:
Full GC影响严重:尤其是在老年代清理时,仍需STW,造成业务请求中断。
大堆内存支持不佳:CMS在大堆场景(数十GB以上)容易产生碎片,甚至OOM。
标记和清理效率有限:并发过程开销大,回收速度不够理想。
因此,为了进一步降低延迟、提升大内存环境下的GC性能,ZGC应运而生,尤其在Java 11之后成为低延迟场景的新宠。
ZGC概述
ZGC(Z Garbage Collector)是Java平台自Java 11起引入的一种可扩展、低延迟、并发型垃圾收集器,旨在为大堆内存场景下的Java应用提供极低的GC暂停时间(最大不超过10ms),同时保持高吞吐量。
在ZGC出现前,虽然G1 GC已实现了对延迟控制的初步优化,但在某些实时性要求极高的系统中(如金融撮合引擎、大型电商、在线游戏服务器等),它仍然无法完全满足毫秒级停顿时间的需求。ZGC正是为此类场景设计。
核心目标
ZGC的主要设计目标如下:
暂停时间不超过10ms(与堆大小无关)
支持超大堆内存(最大支持16TB)
并发回收、并发压缩
低吞吐量损失
低内存碎片率
这些特性使得ZGC非常适用于以下应用场景:
低延迟应用(例如在线交易系统)
大型数据处理(大内存服务端)
响应时间敏感的分布式系统
Java版本支持情况
ZGC最早以实验性功能形式在JDK 11中引入,之后不断发展完善:
Java 版本 | 状态 | 启用方式 |
---|---|---|
JDK 11 | 实验性 | -XX:+UnlockExperimentalVMOptions -XX:+UseZGC |
JDK 15 | 正式支持 | -XX:+UseZGC |
JDK 17 | 长期支持(LTS) | -XX:+UseZGC |
JDK 21 | 引入Generational ZGC | -XX:+UseZGC (自动使用代际ZGC) |
注意:ZGC不支持Java 8。在JDK 8中,建议使用G1 GC作为替代方案来控制延迟,但G1在最小停顿时间方面远不如ZGC。
如何启用ZGC
在JDK 11中启用ZGC需显式解锁实验性选项:
java \-XX:+UnlockExperimentalVMOptions \-XX:+UseZGC \-Xmx8g \-Xms8g \-jar myapp.jar
从JDK 15起,ZGC已成为正式功能,无需再解锁实验性选项:
java \-XX:+UseZGC \-Xmx16g \-Xms16g \-jar myapp.jar
建议在中大型内存(如4GB以上)下使用ZGC以体现其优势。
ZGC命名的由来
ZGC中的"Z"并没有官方明确解释,但社区中普遍认为其含义为:
Zero Pause GC(零停顿GC)
或者表示最终GC的终极目标(The last GC you'll ever need)
ZGC架构深度分析
ZGC之所以能够实现低于10ms的暂停时间,离不开其创新性的内部架构设计。ZGC彻底颠覆了以往垃圾收集器在内存布局和对象访问上的方式,采用以下关键技术:
区域化堆(Region-based Heap)
彩色指针(Colored Pointers)
加载屏障(Load Barriers)
并发回收机制
本节将逐一详细讲解这些核心模块。本部分为第一部分,重点解析:
1. 区域化堆(Region-based Heap)
传统GC如CMS和Parallel GC使用固定大小的堆区域分代(如Eden区、Survivor区、Old区),但ZGC则摒弃了这种固定代分区方式,采用区域化(Region-based)堆布局。
ZGC的堆被动态划分成一块块的逻辑小区域(region),每个区域最小为2MB,最大可以根据配置扩展。这些区域根据用途被分类如下:
Small Object Space:存储小对象,一般小于256KB。
Large Object Space:存储大对象,单个对象跨多个区域。
Remapped Space:用于搬迁中的对象区域(relocation)。
ZGC的region是非连续、非固定映射的,具有以下优势:
堆可动态增长和收缩,极大提升内存使用弹性。
对不同区域类型可采用不同的压缩或回收策略。
支持并发搬迁与并发压缩,减少碎片和延迟。
示例:
// 模拟大对象分配时,ZGC自动从 Large Object Space 中分配区域
byte[] largeArray = new byte[10 * 1024 * 1024]; // 10MB 数组
ZGC可快速在Large Object Space中定位空闲Region,支持高并发申请。
2. 彩色指针(Colored Pointers)
ZGC使用的一项革命性技术是彩色指针,即将对象引用的高位用于标记GC状态信息。这种做法打破了传统将引用与元数据分离的限制。
ZGC的对象指针是64位,但由于现代操作系统通常只用低48位寻址,ZGC利用高位中的几位嵌入颜色信息:
位段 | 含义 |
---|---|
0-47 | 实际地址 |
48 | Finalizable标记位 |
49 | Remapped标记位 |
50 | Marked标记位 |
51 | Load Barrier位 |
通过这种设计,ZGC可以在访问对象指针的瞬间就获知其GC状态,而无需查找外部元数据结构,大大提升了并发访问的性能。
优点包括:
减少GC元信息结构依赖
提升GC期间并发可达性分析速度
降低堆碎片率和对象搬迁时的同步成本
示例说明:
Object ref = obj; // ref 实际上是带颜色标记的指针
开发者无需干预,ZGC自动对这些指针做屏障处理和位标记解码。
3. 加载屏障(Load Barrier)
**加载屏障(Load Barrier)**是ZGC最独特的技术之一。它在每次访问Java对象引用时自动触发,用于处理对象在搬迁过程中的一致性问题。
加载屏障的主要职责是:
判断引用对象是否已被搬迁(relocated)
如果是,执行指针修复(pointer remapping)
保证所有线程访问到的是最新的对象地址
ZGC加载屏障是在JVM层面插入的,对开发者完全透明,不需要修改应用代码。其实现通常依赖CPU原语(如内存屏障)结合内联汇编,实现高效的指针判断与更新。
加载屏障的逻辑类似如下伪代码:
Object ref = load(o);
if (ref has relocation flag) {ref = remap(ref);
}
return ref;
优势:
极低延迟:加载屏障可与普通对象访问融合,不增加明显开销
并发友好:允许对象在不暂停应用线程的前提下完成搬迁
精确控制:每次读取都能精准判断是否需要修复
真实案例场景:
List<Person> people = ...;
for (Person p : people) {// 访问 p.getName() 时会触发 Load Barrier 检查其对象是否已搬迁System.out.println(p.getName());
}
即使此时ZGC正在并发地搬迁 Person
对象,也不会阻塞当前线程读取。
4. 并发回收机制
ZGC之所以能将GC暂停控制在10ms以内,根本在于其极致的并发垃圾回收机制。它几乎将所有GC阶段转为并发执行,避免了传统GC中长时间的"Stop-The-World"。
ZGC的垃圾回收周期包含如下阶段:
阶段 | 是否并发 | 描述 |
---|---|---|
初始标记(Pause Mark Start) | 否 | 极短暂停,标记GC Root |
并发标记(Concurrent Mark) | 是 | 并发遍历整个堆,标记存活对象 |
并发重定位准备(Concurrent Prepare Relocate) | 是 | 选择要搬迁的对象 |
暂停重定位开始(Pause Relocate Start) | 否 | 短暂停,开启重定位 |
并发搬迁(Concurrent Relocate) | 是 | 将活跃对象搬迁至新区域 |
并发重映射(Concurrent Remap) | 是 | 更新所有引用为新地址(结合Load Barrier) |
其中,两个短暂停阶段(初始标记与重定位开始)通常耗时都小于2ms。
ZGC的设计核心是将搬迁(对象复制)也并发完成,这在传统GC中几乎是不可想象的。
示例流程图(文字描述):
GC线程开始并发标记阶段,与应用线程并行运行。
找到垃圾对象集合后,选择部分区域进行回收。
搬迁对象至新区域,由多个线程协作完成,应用线程继续运行。
通过加载屏障和指针重映射,确保应用访问到的是新地址。
这种模式极大减少了STW(Stop-The-World)对应用性能的影响。
优势总结:
几乎全程并发执行
极低暂停时间(<10ms)
支持TB级堆空间的低延迟回收
精细控制搬迁单元,避免大块复制阻塞
ZGC工作原理
ZGC的基本目标是在实现大内存空间支持下,并俗降低GC暂停时间,尽量将GC各阶段转为并发执行。下面将以ZGC一次完整GC周期为线程,分段解析其工作流程:
1. GC触发
ZGC与其他GC一样,通过内存占用分析来触发GC。其GC触发可能原因包括:
内存占用超过阀值
对象分配失败
手动调用
System.gc()
在GC触发后,ZGC进入一次完整的GC周期。
2. 初始标记 (Pause Mark Start)
这是ZGC兩个短暂停阶段之一,将GC Root(如程序栈、静态对象、JNI指针) 加入标记集合。
特点:
更新系统中所有根引用
更新开始标记的标志位(带有颜色的指针)
优化後通常耗时<1ms
3. 并发标记 (Concurrent Mark)
将基于标记集合的引用给所有可达对象进行并发添加。此阶段与应用程序同时运行,不需要暂停。
内部机制:
利用带颜色指针判定对象是否已标记
培子线程分布标记任务,支持核心级并发
并行识别、合并图结构
// 类似于每个对象被标记为活跃时,就会追踪其引用
if (!isMarked(obj)) {mark(obj);for (Object ref : obj.getReferences()) {mark(ref);}
}
4. 并发转移准备 (Concurrent Prepare Relocate)
此阶段分析哪些Region需要转移,通常选择废物比例高的Region,尽量减少拷贝量,提高性能。
标记结束后,定义要移动的Region集合
与应用程序并行
5. 移动开始暂停 (Pause Relocate Start)
为了保证移动阶段的一致性,需要简短地暂停一下,切换GC状态,启用转移。
暂停平均耗时也很短(<2ms)
进行新的Region空间创建
6. 并发对象移动 (Concurrent Relocate)
ZGC使用并发线程将活跃对象转移到新的Region。这些操作与应用程序同时进行,合作Load Barrier确保引用不算错。
// Load Barrier检测到指针已移动
if (isForwarded(ptr)) {ptr = loadForwardingPointer(ptr); // 修复指针
}
7. 并发重映 (Concurrent Remap)
在应用运行过程中,某些指针可能还未被修复,此阶段将通过并发线程把还未被更新的指针重新映射。
重映是指针修复的最后阶段
确保所有引用指向正确对象
8. GC结束
当所有移动完成、指针已更新后,GC周期结束,释放被固定重映的老Region,新Region补入。
ZGC与Java 8中垃圾收集器的对比
在Java 8中,常用的几种垃圾收集器包括Serial GC、Parallel GC、CMS(Concurrent Mark-Sweep)以及G1(Garbage First)GC。这些收集器在当时各有优劣,而ZGC自Java 11引入后,彻底改变了GC在延迟敏感场景下的表现。本节将ZGC与Java 8时代代表性的垃圾收集器——G1、CMS等进行对比,从多个维度全面展示ZGC的优势与局限。
1. 暂停时间对比
收集器 | 暂停类型 | 最佳暂停时间 | 典型暂停时间 | 最差暂停时间 |
---|---|---|---|---|
Serial | Stop-the-World | 10ms - 数百ms | 数十ms - 秒级 | 秒级 |
Parallel | Stop-the-World | 数十ms | 数百ms | 数秒 |
CMS | 并发标记 + STW | 几十ms | 数百ms | 可能超过1s(碎片整理) |
G1 | 分区化,部分并发 | 低于200ms(可设定) | 50ms - 200ms | 秒级(Full GC) |
ZGC | 几乎全并发 | <1ms | <10ms | 通常不超过10ms |
ZGC的暂停时间控制极其优秀,甚至在TB级堆上依然稳定控制在10ms以内。
2. 吞吐量与堆规模支持
收集器 | 最大支持堆大小 | 吞吐能力 |
Serial | 小于8GB | 较低 |
Parallel | 几十GB | 高 |
CMS | 通常推荐<100GB | 中高 |
G1 | 理论上可到数百GB | 中高(根据Region粒度变化) |
ZGC | 实际支持高达数TB | 高(并发、分层) |
ZGC特别适用于超大堆应用,如大数据平台、实时分析引擎、AI在线推理服务等。
3. 并发能力
收集器 | 并发标记 | 并发清理/压缩 | 对应用线程干扰 |
Serial | 否 | 否 | 高 |
Parallel | 否 | 否 | 高 |
CMS | 是 | 部分并发 | 中 |
G1 | 是 | 否(压缩是STW) | 中 |
ZGC | 是 | 是(全阶段并发) | 极低 |
ZGC是真正意义上"全阶段并发"的垃圾收集器,大幅减小GC对响应时间的干扰。
4. 内存碎片处理
CMS 是非压缩的,容易出现内存碎片,导致分配失败。
G1 虽然分区化,但压缩仍需STW,内存整理成本高。
ZGC 利用并发搬迁机制,可以在线完成对象压缩,极少碎片产生。
// G1碎片整理常常需要Stop-the-World:
-XX:+UseG1GC
-XX:+G1HeapRegionSize=8m
// 遇到Old区不足可能引发 Full GC 暂停
5. 部署与兼容性
收集器 | Java版本支持 | 启用方式 |
CMS | Java 8 - Java 14(后移除) | -XX:+UseConcMarkSweepGC |
G1 | Java 7+ | 默认GC(Java 9+) |
ZGC | Java 11+(JDK 15转为生产) |
-XX:+UnlockExperimentalVMOptions
-XX:+UseZGC
``` |ZGC从JDK 15起被标记为生产可用,推荐用于延迟敏感、堆空间巨大的现代系统。### 6. 代码透明度与可维护性ZGC对开发者完全透明,不需要特殊编码配合,不影响业务逻辑。```java
public class User {private String name;private Address address;
}// 正常使用,不需要关注GC过程:
User u = new User();
u.setName("张三");
相比之下,CMS可能因碎片化或过时参数带来配置难度,而G1对GC参数的调优要求较高。
总结:为何选ZGC?
ZGC在以下场景中极具优势:
低延迟要求:如金融交易撮合系统、在线推荐、游戏服务端
大内存平台:如TB级堆数据仓库、机器学习推理服务、海量会话保持
高并发业务:如大型API网关、消息中间件
同时,ZGC的易用性(代码透明、自动压缩、稳定暂停时间)也大大降低了运维与开发门槛,是Java未来GC的核心发展方向之一。
ZGC性能调优指南
ZGC虽然拥有优秀的默认性能表现,但在特定业务场景中,通过合理调优可以进一步提升其效率,降低资源占用,增强服务稳定性。本节将围绕ZGC的参数设置、性能监控、常见问题应对策略进行系统讲解。
一、ZGC启用与基础配置
ZGC需Java 11及以上版本支持,启用ZGC基本参数如下:
# 启用ZGC(Java 11中仍为实验特性)
-XX:+UnlockExperimentalVMOptions
-XX:+UseZGC# 示例:设置最大堆、初始堆大小
-Xmx16g
-Xms16g
在JDK 15及以上版本,无需再解锁实验选项,可直接使用 -XX:+UseZGC
。
二、核心调优参数解析
1. 堆空间设置
ZGC不像G1那样依赖分区大小调优,它的核心是Region自动管理。因此主要关注以下两个参数:
-Xmx16g # 最大堆内存
-Xms16g # 初始堆内存
建议:ZGC在大堆(如>8G)下性能更优,最好将 -Xmx 与 -Xms 设为相同,避免运行时堆调整。
2. GC线程数控制
-XX:ConcGCThreads=N # 控制并发GC线程数量
-XX:ParallelGCThreads=N # 初始标记和对象拷贝时的并行线程数
ZGC自动选择线程数,但在高并发系统中,如需控制资源消耗可手动设定。
3. 启用 NUMA 感知(多核性能优化)
-XX:+UseNUMA
在多Socket架构服务器上建议启用,提升跨节点堆访问性能。
4. 禁用透明大页(降低TLB抖动)
-XX:+UseTransparentHugePages=false
可避免ZGC在大型对象分配中引发频繁页表转换开销。
三、ZGC特有诊断与追踪参数
ZGC支持详细的垃圾回收日志输出,有助于观察其行为与性能:
-Xlog:gc*,safepoint:file=gc.log:time,uptime,level,tags
样例输出:
[2.344s][info][gc,start] GC(0) Pause Mark Start
[2.344s][info][gc] GC(0) Pause Mark Start 0.415ms
[2.345s][info][gc,start] GC(0) Concurrent Mark
[2.567s][info][gc] GC(0) Concurrent Mark 222.187ms
...
通过这些日志,可判断暂停时间是否稳定,GC是否频繁触发等信息。
四、常见调优策略
场景 | 调优建议 |
---|---|
吞吐不足 | 提升GC线程数,增加CPU核心数 |
GC频繁 | 检查内存是否足够,提升 -Xmx 值 |
暂停波动大 | 检查是否开启了大页,是否存在频繁Full GC |
CPU占用高 | 适当限制 GC 并发线程数 |
堆未满就GC | 检查是否被显式调用 System.gc(),避免误触发 |
五、结合容器环境的参数配置建议
在Kubernetes、Docker等容器中运行时,应注意:
-XX:+UseContainerSupport # 启用容器资源感知(JDK 10+ 默认开启)
-XX:MaxRAMPercentage=80.0 # 最大可用内存占比(替代传统的 -Xmx)
这样可确保ZGC在容器内合理管理资源,不会溢出宿主机。