JVM垃圾回收深度解析:G1/ZGC/Shenandoah原理、日志分析与STW优化
本文深入剖析JVM三大现代垃圾回收器,提供生产级调优案例,帮助你将STW停顿从秒级降至毫秒级。
一、算法原理深度剖析
1.1 G1(Garbage First)算法原理
设计目标:平衡吞吐量与停顿时间,可预测的停顿(<200ms)
堆内存布局:
┌─────────────────────────────────────────────────────┐
│ G1 Heap(按Region组织) │
│ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │Eden│ │Eden│ │Survivor│ │Survivor│ │Old│ │Humongous│ │
│ │Region│ │Region│ │Region│ │Region│ │Region│ │Region│ │
│ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘ │
│ (Young Generation) (Tenured Generation) │
└─────────────────────────────────────────────────────┘
- Region:大小1MB-32MB(2的幂),堆被划分为2048个Region
- Humongous Region:存储大对象(>Region 50%),直接分配到老年代
回收过程(Mixed GC):
初始标记(Initial Mark):
- 标记GC Roots直接可达对象
- STW,耗时极短(<1ms)
- 复用Young GC的STW
并发标记(Concurrent Mark):
- 从GC Roots遍历整个堆图
- 并发执行,标记存活对象
- 使用SATB(Snapshot-At-The-Beginning)算法解决浮动垃圾
最终标记(Final Mark):
- STW,处理SATB缓冲区
- 引用处理(Reference Processing)
筛选回收(Evacuation):
- STW,复制存活对象到新的Region
- 优先回收垃圾最多的Region(Garbage First名称由来)
- 可通过
-XX:MaxGCPauseMillis=100控制
核心参数:
bash
# 启用G1(JDK 9+默认)
-XX:+UseG1GC -Xmx16g -Xms16g
# 目标停顿时间(默认200ms,调优关键)
-XX:MaxGCPauseMillis=100
# 并发线程数(建议CPU核数50%)
-XX:ConcGCThreads=4
# 触发Mixed GC的堆占用阈值(默认45%)
-XX:InitiatingHeapOccupancyPercent=35
# 大对象阈值(避免Humongous对象)
-XX:G1HeapRegionSize=16m -XX:G1MixedGCLiveThresholdPercent=85
1.2 ZGC(Z Garbage Collector)算法原理
设计目标:停顿时间<1ms,堆大小从8MB到16TB
核心创新:
- 染色指针(Colored Pointers):在指针中存储元数据,减少内存屏障
- 读屏障(Load Barrier):标记阶段在对象访问时触发
- 并发整理:所有GC阶段(标记/转移/重映射)均可并发
内存布局:
┌─────────────────────────────────────────┐
│ ZGC Virtual Address Space │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Small │ │ Medium │ │ Large │ │
│ │ (2MB) │ │ (32MB) │ │ (>32MB) │ │
│ └─────────┘ └─────────┘ └─────────┘ │
□ 多映射(Multi-Mapping):同一物理内存映射到不同虚拟地址
└─────────────────────────────────────────┘
回收周期:
- 标记(Mark):并发标记存活对象,STW<1ms
- 转移(Relocate):并发复制对象到新Page,STW<1ms
- 重映射(Remap):更新指针引用,并发执行
性能突破:
- 停顿时间不随堆增大而增长:16GB和16TB堆停顿时间相同
- 无分代设计:简化逻辑,但吞吐量略低于G1
生产参数:
bash
# JDK 15+正式可用,JDK 21+推荐
-XX:+UseZGC -Xmx64g -Xms64g
# JDK 21分代ZGC(性能更优)
-XX:+UseZGC -XX:+ZGenerational
# 并发线程数(默认CPU核数1/4,可手动指定)
-XX:ConcGCThreads=8
# 诊断参数
-Xlog:gc*:file=/tmp/zgc.log:time,level,tags:filecount=10,filesize=100M
1.3 Shenandoah算法原理
设计目标:低停顿(10-50ms),适用于大堆
核心机制:
- Brooks Forwarding Pointer:每个对象头增加转发指针,实现并发复制
- 读屏障 + 写屏障:保证对象访问一致性
回收阶段:
Init Mark → Concurrent Mark → Final Mark → Concurrent Cleanup
→ Concurrent Evacuation → Final Update Refs → Concurrent Update Refs
与ZGC对比:
- 吞吐量:Shenandoah > ZGC(约高5-10%)
- 停顿时间:ZGC < Shenandoah(ZGC可达亚毫秒)
- 社区支持:ZGC(Oracle官方)> Shenandoah(RedHat)
生产参数:
bash
# JDK 12+实验,JDK 15+生产就绪
-XX:+UseShenandoahGC -Xmx32g
# 目标停顿时间(默认15ms)
-XX:ShenandoahGCHeuristics=adaptive
-XX:ShenandoahMinFreeThreshold=10
二、GC日志深度分析
2.1 日志开启方式
bash
# G1日志配置(JDK 9+)
-Xlog:gc*:file=/tmp/gc.log:time,level,tags:filecount=10,filesize=100M
# ZGC日志配置(JDK 15+)
-Xlog:gc*,gc+ref*,gc+reloc*,gc+heap*:file=/tmp/zgc.log:time,level,tags:filecount=10,filesize=100M
# Shenandoah日志
-Xlog:gc*,gc+stats:file=/tmp/shenandoah.log:time,level,tags
2.2 G1日志解读
关键日志样例:
[2024-12-19T10:30:00.123+0800][gc,start ] GC(12) Pause Young (Normal) (G1 Evacuation Pause)
[2024-12-19T10:30:00.124+0800][gc,task ] GC(12) Using 4 workers of 8 for evacuation
[2024-12-19T10:30:00.134+0800][gc,phases ] GC(12) Pre Evacuate Collection Set: 0.2ms
[2024-12-19T10:30:00.135+0800][gc,phases ] GC(12) Merge Heap Roots: 0.3ms
[2024-12-19T10:30:00.136+0800][gc,phases ] GC(12) Evacuate Collection Set: 8.5ms ← STW核心耗时
[2024-12-19T10:30:00.137+0800][gc,phases ] GC(12) Post Evacuate Collection Set: 1.2ms
[2024-12-19T10:30:00.138+0800][gc,phases ] GC(12) Other: 0.5ms
[2024-12-19T10:30:00.139+0800][gc,heap ] GC(12) Eden: 12288.0M(12288.0M)->0.0B(13312.0M)
[2024-12-19T10:30:00.140+0800][gc,heap ] GC(12) Survivors: 1024.0M->1536.0M
[2024-12-19T10:30:00.141+0800][gc,heap ] GC(12) Old: 20480.0M(20480.0M)->20480.0M(20480.0M)
[2024-12-19T10:30:00.142+0800][gc,metaspace] GC(12) Metaspace: 256M->256M(512M)
[2024-12-19T10:30:00.143+0800][gc ] GC(12) Pause Young (Normal) (G1 Evacuation Pause) 13312M->22016M(32768M) 10.7ms ← 总STW时间
关键指标:
- Eden区:从12288M→0B,年轻代全部回收
- Survivors:从1024M→1536M,部分对象晋升
- Old区:20480M保持不变(Mixed GC会处理)
- STW时间 :10.7ms(符合
-XX:MaxGCPauseMillis=100)
问题诊断:
- STW > 100ms:检查Evacuate Collection Set耗时,可能存活对象过多
- Old区持续增长 :触发Mixed GC阈值,调整
-XX:InitiatingHeapOccupancyPercent
2.3 ZGC日志解读
关键日志样例:
[2024-12-19T10:30:00.123+0800][gc,phases] GC(123) Pause Mark Start 0.5ms ← 标记开始STW
[2024-12-19T10:30:00.124+0800][gc,phases] GC(123) Concurrent Mark 15.2ms ← 并发标记
[2024-12-19T10:30:00.125+0800][gc,phases] GC(123) Pause Mark End 0.8ms ← 标记结束STW
[2024-12-19T10:30:00.126+0800][gc,phases] GC(123) Concurrent Relocate 20.5ms ← 并发转移
[2024-12-19T10:30:00.127+0800][gc,phases] GC(123) Pause Relocate Start 0.3ms
[2024-12-19T10:30:00.128+0800][gc ] GC(123) Garbage Collection (Warmup) 65536M(65536M)->20480M(65536M) 37.3ms
核心指标:
- Pause Mark Start/End:两次STW均<1ms
- Concurrent阶段:标记和转移并行执行
- 堆大小变化:65536M→20480M,回收40GB垃圾
问题诊断:
- STW > 1ms:检查系统负载,可能CPU资源不足
- Concurrent耗时过长 :调整
-XX:ConcGCThreads增加并发线程
2.4 日志分析工具
GCEasy(在线工具):
bash
# 上传GC日志到 https://gceasy.io/
# 自动生成报告:吞吐量、延迟、内存泄漏分析
GCViewer(开源工具):
bash
java -jar gcviewer.jar g1.log
# 可视化GC频率、停顿时间、内存趋势
IntelliJ JVM Debugger Memory View:
- Debug时实时查看对象内存占用
- 追踪对象引用链
三、生产调优案例
案例1:电商大促G1调优(减少STW 90%)
背景:JDK 8 + ParallelGC,Full GC停顿5秒,高峰期频繁触发
优化前:
bash
-Xmx32g -Xms32g -XX:+UseParallelGC
# GC日志:Full GC 5秒,Young GC 500ms,每小时触发10次Full GC
调优步骤:
Step 1:切换到G1
bash
-Xmx32g -Xms32g -XX:+UseG1GC -XX:MaxGCPauseMillis=100
# STW降至200ms,但仍有Full GC
Step 2:调整Mixed GC触发阈值
bash
-XX:InitiatingHeapOccupancyPercent=30 # 默认45%,提前触发Mixed GC
-XX:G1MixedGCLiveThresholdPercent=85 # 回收存活率<85%的Old Region
-XX:G1MixedGCCountTarget=8 # 8次Mixed GC完成回收
Step 3:优化大对象处理
bash
-XX:G1HeapRegionSize=16m # 增大Region,减少Humongous对象
-XX:G1ReservePercent=15 # 预留15%空间防止晋升失败
Step 4:GC日志验证
# 优化后日志
[gc] GC(1024) Pause Young (Mixed) (G1 Evacuation Pause) 24576M->18432M(32768M) 45ms
[gc] GC(1025) Pause Young (Mixed) (G1 Evacuation Pause) 22528M->17408M(32768M) 38ms
[gc] GC(1026) Pause Young (Concurrent Start) (G1 Humongous Allocation) 18432M->16384M(32768M) 52ms
- Young GC:50ms以内
- Mixed GC:40ms,无Full GC
- STW减少 :从5秒→50ms,减少99%
案例2:金融交易ZGC调优(STW<1ms)
背景:低延迟交易系统,要求停顿<10ms,堆内存64GB
配置:
bash
-Xmx64g -Xms64g -XX:+UseZGC -XX:ConcGCThreads=8
压力测试:
java
// 模拟高并发订单处理
@Benchmark
public void processOrder() {
byte[] data = new byte[1024 * 10]; // 生成临时对象
// 业务逻辑
}
优化结果:
# GC日志分析
[gc] GC(50142) Pause Mark Start 0.3ms
[gc] GC(50142) Pause Mark End 0.5ms
[gc] GC(50142) Garbage Collection (Allocation Rate) 61440M->20480M(65536M) 15.2ms
# 指标
- 平均停顿:0.4ms
- 99.9%停顿:<1ms
- 吞吐量:98.5%(几乎无GC影响)
案例3:Shenandoah调优(吞吐量优先)
场景:大数据计算平台,堆128GB,要求吞吐>95%
配置:
bash
-Xmx128g -XX:+UseShenandoahGC -XX:ShenandoahGCHeuristics=adaptive -XX:ParallelGCThreads=16
调优:
bash
-XX:ShenandoahFreeThreshold=5 # 当空闲Region<5%时触发GC
-XX:ShenandoahGuaranteedGCInterval=600000 # 每10分钟强制GC
结果:
- 停顿时间:15-30ms
- 吞吐量:96.2%
- GC周期:每5-8分钟一次
四、STW减少90%的核心策略
策略1:选择合适的GC
yaml
场景选择:
通用Web应用: G1 # 平衡吞吐与延迟
低延迟(10ms): ZGC # 亚毫秒停顿
大堆+高吞吐: Shenandoah # 停顿<50ms+高吞吐
传统应用: ParallelGC # JDK 8遗留
策略2:减少对象分配
java
// 优化前:频繁创建临时对象
String result = "";
for (User user : users) {
result += user.getName(); // 创建大量String对象
}
// 优化后:重用对象
StringBuilder sb = new StringBuilder(1024);
for (User user : users) {
sb.append(user.getName());
}
// 减少Young GC频率50%
策略3:调整GC线程数
bash
# G1:并发线程=CPU核数50%
-XX:ConcGCThreads=8 # 16核CPU
# ZGC:默认1/4,可手动增大
-XX:ConcGCThreads=12
# 避免GC线程过多导致CPU竞争
策略4:增大Region/HeapRegionSize
bash
# 减少Humongous对象
-XX:G1HeapRegionSize=16m # 默认根据堆自动计算
# 效果:大对象判定阈值从Region的50%提升,减少Humongous分配
策略5:优化堆内存分配
bash
# 设置合理的新生代比例
-XX:G1NewSizePercent=30 # 新生代最小比例(默认5%)
-XX:G1MaxNewSizePercent=60 # 新生代最大比例(默认60%)
# 避免新生代过小导致频繁GC
策略6:使用Native Memory Tracking
bash
-XX:NativeMemoryTracking=summary -XX:+UnlockDiagnosticVMOptions -XX:+PrintNMTStatistics
# 监控堆外内存(DirectByteBuffer),避免堆外OOM
策略7:预热与压测
bash
# 应用启动后立即触发GC,预热堆空间
java -XX:+UseG1GC -XX:G1ConcRefinementThreads=4 -jar app.jar &
sleep 30 && jcmd <pid> GC.run
# 压测工具:JMH + GCProfiler
五、监控与告警
Prometheus + Grafana监控
JVM Exporter配置:
yaml
# docker-compose.yml
jmx_exporter:
image: sscaling/jmx-prometheus-exporter
ports:
- "5556:5556"
environment:
JVM_OPTS: "-javaagent:/jmx_prometheus_javaagent.jar=5556:/config.yml"
Grafana大盘关键指标:
- GC停顿时间 :
rate(jvm_gc_pause_seconds_sum[5m]) - GC频率 :
rate(jvm_gc_pause_seconds_count[5m]) - 堆使用率 :
(jvm_memory_bytes_used / jvm_memory_bytes_max) * 100 - 告警阈值:停顿>100ms或频率>10次/分钟
自定义GC健康检查
java
@Component
public class GcHealthIndicator implements HealthIndicator {
@Override
public Health health() {
long totalGcTime = ManagementFactory.getGarbageCollectorMXBeans().stream()
.mapToLong(GarbageCollectorMXBean::getCollectionTime)
.sum();
if (totalGcTime > 1000) { // 1秒内GC时间超过1秒
return Health.down().withDetail("gcTime", totalGcTime).build();
}
return Health.up().build();
}
}
总结
调优黄金法则
- 监控先行:无监控不调优
- 逐步调整:一次只改一个参数,对比效果
- 压实验证:生产配置必须在压测环境验证
- 日志留痕:保留调优前后的GC日志
三种GC选型决策树
需要停顿<1ms? → 是 → ZGC
↓否
需要停顿<10ms且大堆? → 是 → Shenandoah
↓否
通用场景 → G1
STW减少90%的关键
- 选对GC:ZGC可减少99%停顿
- 减少分配:对象池、StringBuilder重用
- 优化参数:MaxGCPauseMillis、InitiatingHeapOccupancyPercent
- 监控闭环:Prometheus + AlertManager实时告警
掌握GC调优是后端专家的分水岭,需要理论与实践结合,持续监控验证。