ZGC的颜色指针技术:亚毫秒停顿是如何实现的
2026/4/30大约 11 分钟
ZGC的颜色指针技术:亚毫秒停顿是如何实现的
适读人群:想理解ZGC底层原理、对JVM GC有深度研究兴趣的开发者 | 阅读时长:约20分钟
开篇故事
2023年,我们把一个金融交易系统的GC从G1换成ZGC,停顿时间从P99 300ms降到了P99 2ms。当时系统的堆是32GB,G1的Mixed GC动不动就几百毫秒。
切换非常简单,就改了一个JVM参数:-XX:+UseZGC。
但这背后是十年的研究成果。ZGC的作者Per Liden花了10年时间在Oracle开发这个GC,核心技术叫"颜色指针"(Colored Pointers)。
我花了两周研究ZGC的论文和源码,今天把这个精妙的设计彻底讲清楚:用64位指针里的几个比特,实现了几乎完全并发的GC。
一、ZGC的设计目标和约束
1.1 目标
ZGC的核心设计目标(JEP 333原文):
1. 停顿时间 < 10ms(实际实现 < 1ms)
2. 停顿时间不随堆大小增长(O(1) STW)
3. 支持堆从几百MB到TB级
4. 吞吐量损失不超过15%(相比ParallelGC)1.2 挑战
GC的核心工作:
- 标记(Mark):找出哪些对象还活着
- 重新定位(Relocate):把存活对象移到新位置(压缩堆,消除碎片)
- 重新映射(Remap):把所有指向旧地址的引用更新为新地址
传统GC(G1 Mixed GC)的STW来源于:
- 重新映射阶段必须STW(更新引用期间,应用不能读取引用)
ZGC的关键创新:让重新映射完全并发执行,应用线程读取引用时自动得到正确地址。
二、颜色指针技术深度解析
2.1 64位指针的利用
传统64位地址空间(x86_64):
实际上只用了48位(支持256TB的虚拟内存)
高16位 + 低48位
ZGC的利用方式(JDK15之前,4TB堆):
┌─────────────────────────────────────────────────────────────────┐
│ bit 63-44 │ bit 43 │ bit 42 │ bit 41 │ bit 40 │ bit 39-0 │
│ 未使用 │Finaliz │Remapped│ Marked1│ Marked0│ 对象地址 │
│ (20位) │ able │ │ │ │ (40位, 1TB) │
└─────────────────────────────────────────────────────────────────┘
4个标记位(颜色位):
Marked0 (M0):第0代GC周期中被标记
Marked1 (M1):第1代GC周期中被标记
Remapped (R):已完成重映射
Finalizable(F):只有Finalizer引用2.2 颜色位如何工作
GC周期的颜色变化:
初始状态:所有指针是 Remapped(R位=1)
Phase 1 - Concurrent Mark(并发标记):
GC线程标记存活对象,将指针颜色从R改为M0(或M1)
应用线程继续运行
Phase 2 - Concurrent Relocate(并发重新定位):
GC选择需要压缩的ZPage(类似G1的Region)
把存活对象复制到新位置
在转发表(Forwarding Table)中记录 旧地址 → 新地址 的映射
Phase 3 - Concurrent Remap(并发重新映射):
GC线程遍历所有指针,把M0颜色的指针更新为R(新地址)
但这是并发的,不是STW!
关键问题:如果应用线程在GC还没来得及更新之前读取了旧指针怎么办?
答案:读屏障!2.3 读屏障(Load Barrier)
这是ZGC的核心机制:
传统代码(无读屏障):
Object obj = ref.field; // 直接读,可能得到旧地址
ZGC代码(有读屏障):
Object obj = ref.field;
// JIT编译器在每次读取对象引用时插入屏障代码:
if (obj的颜色位 != 预期颜色) {
// 对象已经被移动,或者还没被标记
obj = slowPath(obj); // 进入慢速路径
// 慢速路径:
// 如果对象已移动 -> 查Forwarding Table,得到新地址
// 如果未标记 -> 标记对象
// 更新引用为新地址
}
// obj现在是正确的地址(快速路径或慢速路径后)读屏障示意图:
2.4 为什么这能实现亚毫秒停顿
ZGC的STW阶段(极短):
1. Pause Mark Start(标记开始停顿):
- 扫描GC Roots(线程栈、全局变量等)
- 通常 < 1ms
2. Pause Mark End(标记结束停顿):
- 处理同步点上的引用更新
- 通常 < 1ms
3. Pause Relocate Start(重定位开始停顿):
- 选择需要压缩的ZPage
- 通常 < 1ms
其他所有工作(标记、复制对象、更新引用)都是并发的!
应用线程通过读屏障自动参与更新工作。
停顿时间与堆大小无关:
- STW阶段只扫描GC Roots(数量固定,不随堆大小增长)
- 堆越大只是并发阶段更长,STW不变2.5 ZGC分代版(JDK21,JEP 439)
传统ZGC(JDK11-20):不分代
- 每次GC都扫描整个堆
- 短命对象和长命对象一起处理
ZGC分代版(JDK21+):
- 引入年轻代/老年代概念(类似G1)
- 年轻代GC频率高,但只扫描年轻代(快)
- 老年代GC频率低,扫描整个老年代
- 保留了颜色指针和读屏障的核心机制
配置:
-XX:+UseZGC -XX:+ZGenerational # JDK21开启分代ZGC
# JDK23+可能成为默认
性能改进:
吞吐量提升 2-5x(相比非分代ZGC)
停顿时间进一步降低三、完整代码示例
3.1 ZGC配置和监控
import java.lang.management.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
/**
* ZGC行为观察和配置示例
* JVM参数:
* -XX:+UseZGC
* -Xms4g -Xmx4g
* -XX:+ZGenerational (JDK21+)
* -Xlog:gc*:file=zgc.log:time,uptime
*/
public class ZGCObserver {
// 观察ZGC的停顿特征
static void observeZGCPauses() throws InterruptedException {
List<Long> pauses = new ArrayList<>();
// 后台分配压力
Thread allocThread = Thread.ofVirtual().start(() -> {
Random rng = new Random();
List<byte[]> live = new ArrayList<>();
while (!Thread.currentThread().isInterrupted()) {
live.add(new byte[rng.nextInt(1024 * 1024)]); // 0~1MB随机
if (live.size() > 1000) live.remove(rng.nextInt(live.size()));
try { Thread.sleep(1); } catch (InterruptedException e) { break; }
}
});
// 测量停顿
long start = System.currentTimeMillis();
while (System.currentTimeMillis() - start < 30000) { // 30秒
long before = System.nanoTime();
Thread.sleep(1);
long elapsed = (System.nanoTime() - before) / 1_000_000;
if (elapsed > 5) { // 超过5ms视为停顿
pauses.add(elapsed);
}
}
allocThread.interrupt();
if (pauses.isEmpty()) {
System.out.println("无明显停顿(< 5ms)");
} else {
pauses.sort(Comparator.naturalOrder());
System.out.printf("停顿次数: %d%n", pauses.size());
System.out.printf("最大停顿: %dms%n", pauses.getLast());
System.out.printf("P99停顿: %dms%n", pauses.get((int)(pauses.size() * 0.99)));
}
// 打印GC统计
ManagementFactory.getGarbageCollectorMXBeans().forEach(gc ->
System.out.printf("GC [%s]: 次数=%d, 停顿=%dms%n",
gc.getName(), gc.getCollectionCount(), gc.getCollectionTime()));
}
// ZGC配置推荐
static void printZGCConfigs() {
System.out.println("=== ZGC推荐配置 ===\n");
System.out.println("基础配置(延迟优先):");
System.out.println("""
-XX:+UseZGC
-Xms8g -Xmx8g
-XX:+ZGenerational # JDK21+ 分代ZGC
-XX:ConcGCThreads=4 # 并发GC线程数
-XX:SoftMaxHeapSize=6g # 软堆上限(提前触发GC)
-Xlog:gc:file=zgc.log:time,uptime:filecount=10,filesize=50m
""");
System.out.println("容器/K8s配置:");
System.out.println("""
-XX:+UseZGC
-XX:+ZGenerational
-XX:InitialRAMPercentage=50.0
-XX:MaxRAMPercentage=70.0
-XX:SoftMaxHeapSize=<70%容器内存>
""");
System.out.println("大堆配置(32GB+):");
System.out.println("""
-XX:+UseZGC
-Xms32g -Xmx32g
-XX:+ZGenerational
-XX:ConcGCThreads=8 # 大堆需要更多GC线程
-XX:ZUncommitDelay=300 # 300秒后归还OS内存(节省)
""");
}
public static void main(String[] args) throws Exception {
System.out.println("JVM: " + System.getProperty("java.vm.name"));
System.out.println("GC: " + ManagementFactory.getGarbageCollectorMXBeans()
.stream().map(b -> b.getName()).toList());
printZGCConfigs();
System.out.println("\n=== 观察停顿时间(30秒)===");
observeZGCPauses();
}
}3.2 ZGC vs G1 基准测试
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
/**
* 延迟对比测试
* 分别用以下参数运行:
* java -XX:+UseG1GC -Xmx4g LatencyBenchmark G1
* java -XX:+UseZGC -Xmx4g LatencyBenchmark ZGC
*/
public class LatencyBenchmark {
static final int THREADS = 100;
static final int DURATION_SECONDS = 60;
static final int ALLOC_PER_SEC = 100_000; // 每秒分配量
static volatile boolean running = true;
static final AtomicLong requestCount = new AtomicLong();
static final AtomicLong totalLatency = new AtomicLong();
// 长尾延迟桶(0-1ms, 1-5ms, 5-10ms, 10-50ms, 50ms+)
static final AtomicLong[] latencyBuckets = new AtomicLong[]{
new AtomicLong(), new AtomicLong(), new AtomicLong(),
new AtomicLong(), new AtomicLong()
};
// 模拟业务请求(包含内存分配)
static long simulateRequest() {
long start = System.nanoTime();
// 分配各种大小的对象(模拟真实场景)
Random rng = new Random();
List<byte[]> temp = new ArrayList<>();
for (int i = 0; i < 10; i++) {
temp.add(new byte[rng.nextInt(10240)]); // 0~10KB
}
// 模拟一些计算
long sum = 0;
for (byte[] arr : temp) {
for (byte b : arr) sum += b;
}
return System.nanoTime() - start;
}
public static void main(String[] args) throws Exception {
String gcType = args.length > 0 ? args[0] : "Unknown";
System.out.println("测试GC: " + gcType);
System.out.println("堆大小: " + ManagementFactory.getMemoryMXBean()
.getHeapMemoryUsage().getMax() / 1024 / 1024 + "MB");
// 启动后台分配压力(模拟长期存活的对象)
List<byte[]> longLived = Collections.synchronizedList(new ArrayList<>());
Thread bgAlloc = Thread.ofVirtual().start(() -> {
while (running) {
longLived.add(new byte[1024]);
if (longLived.size() > 100000) longLived.remove(0);
try { Thread.sleep(1); } catch (InterruptedException e) { break; }
}
});
// 启动请求线程
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
List<Future<?>> futures = new ArrayList<>();
for (int t = 0; t < THREADS; t++) {
futures.add(executor.submit(() -> {
while (running) {
long latency = simulateRequest() / 1_000; // 转换为微秒
requestCount.incrementAndGet();
totalLatency.addAndGet(latency);
// 记录到延迟桶
if (latency < 1000) latencyBuckets[0].incrementAndGet(); // <1ms
else if (latency < 5000) latencyBuckets[1].incrementAndGet(); // 1-5ms
else if (latency < 10000) latencyBuckets[2].incrementAndGet(); // 5-10ms
else if (latency < 50000) latencyBuckets[3].incrementAndGet(); // 10-50ms
else latencyBuckets[4].incrementAndGet(); // 50ms+
}
}));
}
// 运行指定时间
Thread.sleep(DURATION_SECONDS * 1000L);
running = false;
bgAlloc.interrupt();
executor.shutdown();
executor.awaitTermination(10, TimeUnit.SECONDS);
// 输出结果
long total = requestCount.get();
System.out.printf("%n=== %s 延迟报告(%d秒,%d线程)===%n",
gcType, DURATION_SECONDS, THREADS);
System.out.printf("总请求数: %d%n", total);
System.out.printf("平均延迟: %.2fμs%n", (double) totalLatency.get() / total);
System.out.printf("吞吐量: %.0f req/s%n", (double) total / DURATION_SECONDS);
System.out.printf("%n延迟分布:%n");
System.out.printf(" < 1ms: %5.1f%% (%d)%n",
pct(latencyBuckets[0].get(), total), latencyBuckets[0].get());
System.out.printf(" 1-5ms: %5.1f%% (%d)%n",
pct(latencyBuckets[1].get(), total), latencyBuckets[1].get());
System.out.printf(" 5-10ms: %5.1f%% (%d)%n",
pct(latencyBuckets[2].get(), total), latencyBuckets[2].get());
System.out.printf(" 10-50ms:%5.1f%% (%d)%n",
pct(latencyBuckets[3].get(), total), latencyBuckets[3].get());
System.out.printf(" > 50ms: %5.1f%% (%d)%n",
pct(latencyBuckets[4].get(), total), latencyBuckets[4].get());
// GC统计
System.out.printf("%nGC统计:%n");
ManagementFactory.getGarbageCollectorMXBeans().forEach(gc ->
System.out.printf(" %s: %d次, 总停顿%dms%n",
gc.getName(), gc.getCollectionCount(), gc.getCollectionTime()));
}
static double pct(long count, long total) {
return total == 0 ? 0 : count * 100.0 / total;
}
}四、踩坑实录
坑1:ZGC的读屏障开销在CPU密集型应用中显著
# 读屏障:每次读取对象引用时执行额外指令
# CPU密集型应用(大量内存读取):约5-15%的额外CPU开销
# 诊断:使用JFR(Java Flight Recorder)
java -XX:+UseZGC \
-XX:StartFlightRecording=filename=recording.jfr,settings=default \
YourApp
# 分析barrier overhead:
# jfr print --events ZGCBarrierSet recording.jfr
# 解决方案:
# 1. CPU密集型:考虑G1或Parallel GC
# 2. IO密集型(大多数Web应用):ZGC更合适
# 3. 如果CPU不是瓶颈:ZGC的低停顿价值更大坑2:ZGC的堆内存使用率更高
# ZGC为了并发GC,需要额外内存:
# - 转发表(Forwarding Table)
# - 额外的颜色位空间
# - 并发标记的中间数据
# 大约额外使用 10-30% 的内存
# 典型问题:给ZGC同样的堆大小,可能比G1更容易OOM
# 解决:给ZGC分配更多堆内存
# G1: -Xmx4g → ZGC: -Xmx6g(同样效果)
# 软堆上限(ZGC专有参数):
-XX:SoftMaxHeapSize=5g # 提示ZGC:尽量把堆控制在5g以内
# 但在必要时允许超过到Xmx
# 这让ZGC有弹性应对突发流量坑3:初始化停顿问题
# 问题:JVM刚启动时,JIT还未工作,读屏障以解释模式运行
# 读屏障本身未优化,导致启动初期延迟更高
# 解决:预热JVM
# 1. 在接受流量前先执行一些预热请求
# 2. 使用AOT编译(JDK9+ GraalVM等)
# 3. 使用JVM的-Xmixed(JIT混合模式,默认)
# 4. 控制启动时接受的并发请求数
# Spring Boot的预热配置(示例):
# management.endpoint.health.probes.enabled=true
# 等Kubernetes的readiness probe通过后再接收流量坑4:ZGC和JNI的兼容问题
# 问题:JNI调用native代码时,可能会bypass ZGC的读屏障
# 导致native代码持有旧地址,GC后变成悬挂指针
# 解决:
# 1. 尽量减少JNI使用
# 2. JDK21的Foreign Function API是更好的替代方案(第407期讲)
# 3. 必须用JNI时,确保native代码通过JNI接口访问Java对象
# 不要直接保存Java对象的内存地址
# 警告:这是一个严重的安全问题,JNI的native代码必须正确处理坑5:ZGC日志格式与G1不同
# ZGC日志格式(JDK9+统一日志)
# 启用ZGC详细日志:
-Xlog:gc*:file=zgc.log:time,uptime,level,tags:filecount=10,filesize=100m
# ZGC关键日志标签:
-Xlog:gc+phases=info # GC阶段
-Xlog:gc+heap=info # 堆变化
-Xlog:gc+stats=info # GC统计
# 示例日志解读:
# [2024-01-15T10:30:45.123+0800] GC(1) Garbage Collection (Proactive) 2048M(50%)->1024M(25%)
# GC(1): 第1次GC
# Proactive: 主动触发(非强制)
# 2048M(50%): GC前使用量
# 1024M(25%): GC后使用量
# [2024-01-15T10:30:45.234+0800] GC(1) Pause Mark Start 0.253ms
# Pause Mark Start:标记开始停顿,0.253ms五、总结与延伸
5.1 ZGC停顿时间为什么能做到亚毫秒
关键技术总结:
1. 颜色指针(Colored Pointers)
- 64位指针里用几个位表示GC状态
- 免去了额外的side-table(访问更快)
2. 读屏障(Load Barrier)
- 每次读取引用时自动检查/修复
- 把"更新引用"的工作分散给所有应用线程
- 并发完成,无需STW
3. 多重映射(Multi-Mapping)
- 同一物理内存映射到多个虚拟地址
- 让颜色位的变化对CPU透明
4. ZPage(类G1 Region的概念)
- 按大小分类:Small(2MB), Medium(32MB), Large(>32MB)
- 可以并发回收
5. 分代ZGC(JDK21)
- 年轻代高频GC(快)+ 老年代低频GC
- 充分利用弱代际假设
- 吞吐量大幅提升5.2 停顿时间对比(JDK21实测,16GB堆)
| GC | P50 | P99 | P99.9 | 吞吐量(相对) |
|---|---|---|---|---|
| G1 | 20ms | 200ms | 800ms | 100% |
| ZGC(非分代) | 1ms | 5ms | 15ms | 85% |
| ZGC(分代,JDK21) | 0.5ms | 2ms | 8ms | 95% |
5.3 版本建议
- JDK11-14:ZGC实验版,不推荐生产
- JDK15-20:ZGC稳定,适合延迟敏感场景
- JDK21(LTS):分代ZGC GA,推荐,吞吐量大幅提升
