垃圾收集器-ZGC

前言

在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在延迟方面取得了一定进展,但仍存在以下痛点:

  1. Full GC影响严重:尤其是在老年代清理时,仍需STW,造成业务请求中断。

  2. 大堆内存支持不佳:CMS在大堆场景(数十GB以上)容易产生碎片,甚至OOM。

  3. 标记和清理效率有限:并发过程开销大,回收速度不够理想。

因此,为了进一步降低延迟、提升大内存环境下的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中几乎是不可想象的。

示例流程图(文字描述)

  1. GC线程开始并发标记阶段,与应用线程并行运行。

  2. 找到垃圾对象集合后,选择部分区域进行回收。

  3. 搬迁对象至新区域,由多个线程协作完成,应用线程继续运行。

  4. 通过加载屏障和指针重映射,确保应用访问到的是新地址。

这种模式极大减少了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在容器内合理管理资源,不会溢出宿主机。