【Java】【JVM】垃圾回收深度解析:G1/ZGC/Shenandoah原理、日志分析与STW优化

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):同一物理内存映射到不同虚拟地址
└─────────────────────────────────────────┘

回收周期

  1. 标记(Mark):并发标记存活对象,STW<1ms
  2. 转移(Relocate):并发复制对象到新Page,STW<1ms
  3. 重映射(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();
    }
}

总结

调优黄金法则

  1. 监控先行:无监控不调优
  2. 逐步调整:一次只改一个参数,对比效果
  3. 压实验证:生产配置必须在压测环境验证
  4. 日志留痕:保留调优前后的GC日志

三种GC选型决策树

复制代码
需要停顿<1ms? → 是 → ZGC
            ↓否
需要停顿<10ms且大堆? → 是 → Shenandoah
            ↓否
通用场景 → G1

STW减少90%的关键

  • 选对GC:ZGC可减少99%停顿
  • 减少分配:对象池、StringBuilder重用
  • 优化参数:MaxGCPauseMillis、InitiatingHeapOccupancyPercent
  • 监控闭环:Prometheus + AlertManager实时告警

掌握GC调优是后端专家的分水岭,需要理论与实践结合,持续监控验证。

相关推荐
xrkhy14 小时前
Java全栈面试题及答案汇总(3)
java·开发语言·面试
SunnyDays101114 小时前
Java 高效实现 CSV 转 PDF
java·csv转pdf
菩提祖师_14 小时前
量子机器学习在时间序列预测中的应用
开发语言·javascript·爬虫·flutter
刘975314 小时前
【第22天】22c#今日小结
开发语言·c#
隐形喷火龙14 小时前
SpringBoot 异步任务持久化方案:崩溃重启不丢任务的完整实现
java·spring boot·后端
我是koten14 小时前
K8s启动pod失败,日志报非法的Jar包排查思路(Invalid or corrupt jarfile /app/xxxx,jar)
java·docker·容器·kubernetes·bash·jar·shell
WX-bisheyuange14 小时前
基于Spring Boot的库存管理系统的设计与实现
java·spring boot·后端
明天好,会的14 小时前
分形生成实验(三):Rust强类型驱动的后端分步实现与编译时契约
开发语言·人工智能·后端·rust
YanDDDeat14 小时前
【JVM】类初始化和加载
java·开发语言·jvm·后端