JVM垃圾回收器选择与调优实战
2024.11.20
Java
8 min
3.1k 字
// 目录 · contents
1. 引言
垃圾回收(Garbage Collection, GC)是JVM最核心的功能之一。选择合适的垃圾回收器并进行恰当的调优,直接影响着应用的吞吐量和响应延迟。随着JDK版本的演进,GC技术也在不断发展,从早期的Serial GC到如今的ZGC和Shenandoah,每一代回收器都在不同维度上做出了改进。
本文将系统梳理各主流GC算法的原理和特点,结合真实的调优案例,帮助读者在实际项目中做出正确的GC选型与调优决策。
2. GC算法基础
2.1 核心算法
所有现代GC都基于以下基础算法或其组合:
graph TB
A[GC基础算法] --> B[标记-清除<br/>Mark-Sweep]
A --> C[标记-复制<br/>Mark-Copy]
A --> D[标记-整理<br/>Mark-Compact]
B --> B1[优点: 实现简单]
B --> B2[缺点: 内存碎片]
C --> C1[优点: 无碎片,分配快]
C --> C2[缺点: 浪费一半空间]
D --> D1[优点: 无碎片,空间利用率高]
D --> D2[缺点: 移动对象开销大]
style A fill:#FFD700,stroke:#333,stroke-width:2px
2.2 分代模型
大多数GC(ZGC和Shenandoah除外)基于分代假说:绝大多数对象朝生暮死,经历多次GC的老年对象更难被回收。
graph LR
subgraph 年轻代 Young Generation
E[Eden] --> S0[Survivor 0]
S0 --> S1[Survivor 1]
end
subgraph 老年代 Old Generation
OLD[Tenured]
end
S1 -->|年龄达到阈值<br/>或Survivor空间不足| OLD
style E fill:#90EE90
style S0 fill:#87CEEB
style S1 fill:#87CEEB
style OLD fill:#DDA0DD
3. 主流垃圾回收器对比
3.1 回收器全景图
graph TB
subgraph 年轻代回收器
Serial[Serial]
ParNew[ParNew]
PSY[Parallel Scavenge]
end
subgraph 老年代回收器
SerialOld[Serial Old]
CMS[CMS]
PO[Parallel Old]
end
subgraph 全区域回收器
G1[G1]
ZGC[ZGC]
SH[Shenandoah]
end
Serial -.->|配合| SerialOld
Serial -.->|配合| CMS
ParNew -.->|配合| SerialOld
ParNew -.->|配合| CMS
PSY -.->|配合| SerialOld
PSY -.->|配合| PO
style G1 fill:#FFD700,stroke:#333,stroke-width:2px
style ZGC fill:#90EE90,stroke:#333,stroke-width:2px
style SH fill:#87CEEB,stroke:#333,stroke-width:2px
3.2 详细对比
| 特性 | Serial | Parallel | CMS | G1 | ZGC | Shenandoah |
|---|---|---|---|---|---|---|
| 并行/并发 | 串行 | 并行 | 并发 | 并行+并发 | 并发 | 并发 |
| 目标 | 简单可靠 | 高吞吐 | 低延迟 | 平衡 | 超低延迟 | 超低延迟 |
| STW暂停 | 长 | 中 | 短(两次短暂STW) | 可控 | <1ms | <10ms |
| 堆大小 | 小堆 | 中大堆 | 中大堆 | 中大堆 | 大堆(TB级) | 大堆 |
| 内存碎片 | 无(整理) | 无(整理) | 有(清除) | 无(整理) | 无 | 无 |
| JDK版本 | 所有 | 所有 | 已废弃(JDK14) | 9+默认 | 15+(正式) | 12+ |
3.3 G1 GC详解
G1(Garbage-First)是JDK 9开始的默认回收器,它将堆划分为大小相等的Region,兼顾吞吐量和延迟:
graph TB
subgraph G1堆内存布局
direction LR
R1[E] --- R2[S] --- R3[O] --- R4[E] --- R5[H]
R6[O] --- R7[E] --- R8[O] --- R9[S] --- R10[空]
R11[O] --- R12[H] --- R13[H] --- R14[E] --- R15[O]
end
L1[E = Eden Region] ~~~ L2[S = Survivor Region]
L3[O = Old Region] ~~~ L4[H = Humongous Region]
style R1 fill:#90EE90
style R4 fill:#90EE90
style R7 fill:#90EE90
style R14 fill:#90EE90
style R2 fill:#87CEEB
style R9 fill:#87CEEB
style R3 fill:#DDA0DD
style R6 fill:#DDA0DD
style R8 fill:#DDA0DD
style R11 fill:#DDA0DD
style R15 fill:#DDA0DD
style R5 fill:#FFB6C1
style R12 fill:#FFB6C1
style R13 fill:#FFB6C1
style R10 fill:#F5F5F5
G1的回收过程:
sequenceDiagram
participant App as 应用线程
participant GC as GC线程
Note over App,GC: Young GC
App->>GC: Eden区满触发
GC->>GC: STW - 复制存活对象到Survivor/Old
GC->>App: 恢复执行
Note over App,GC: Mixed GC (并发标记后)
App->>GC: 堆占用达到阈值(IHOP)
GC->>GC: 初始标记(STW,搭载Young GC)
GC-->>App: 并发标记(与应用并行)
GC->>GC: 最终标记(STW)
GC->>GC: 筛选回收(STW,选择收益最高的Region)
GC->>App: 恢复执行
3.4 ZGC详解
ZGC是JDK 15正式发布的超低延迟回收器,核心特点是暂停时间不超过1毫秒且不随堆大小增长:
graph TB
subgraph ZGC核心技术
CP[染色指针<br/>Colored Pointers] --> CP1[在指针中嵌入GC元数据]
CP --> CP2[64位指针中使用4个标记位]
LB[读屏障<br/>Load Barriers] --> LB1[加载引用时检查标记位]
LB --> LB2[实现并发转移时的自愈]
RM[Region化内存<br/>ZPages] --> RM1[Small: 2MB]
RM --> RM2[Medium: 32MB]
RM --> RM3[Large: N*2MB]
end
style CP fill:#FFD700
style LB fill:#87CEEB
style RM fill:#90EE90
4. GC调优参数
4.1 通用参数
1 | |
4.2 G1调优参数
1 | |
4.3 ZGC调优参数
1 | |
5. GC监控工具
5.1 jstat实时监控
1 | |
5.2 jmap堆分析
1 | |
5.3 GC日志分析
推荐使用GCEasy(https://gceasy.io)或GCViewer等工具分析GC日志:
1 | |
6. 实战案例:电商系统GC调优
6.1 问题背景
某电商平台在大促期间出现间歇性接口超时,P99延迟飙升到2秒以上。通过监控发现是GC暂停导致的。
系统配置: - JDK 11,8核16GB服务器 -
堆内存:-Xms8g -Xmx8g - 默认G1 GC,未做任何调优
6.2 问题分析
flowchart TB
S1[现象: P99延迟飙升] --> S2[分析GC日志]
S2 --> S3{GC暂停类型?}
S3 -->|Mixed GC暂停800ms| S4[分析Mixed GC]
S3 -->|偶发Full GC 2s+| S5[分析Full GC原因]
S4 --> S4a[老年代Region回收过多<br/>每次Mixed GC回收200+个Region]
S5 --> S5a[大对象直接分配到Humongous Region<br/>触发Full GC]
S4a --> T1[调整MaxGCPauseMillis]
S5a --> T2[调整G1HeapRegionSize<br/>减少Humongous分配]
style S1 fill:#FF6347,color:#fff
style T1 fill:#90EE90
style T2 fill:#90EE90
通过GC日志分析,发现两个核心问题:
- Mixed GC暂停过长:每次Mixed GC回收过多Region,单次暂停达800ms
- 频繁Humongous分配:系统中存在大量4MB以上的报文对象,超过Region大小的一半(默认Region=4MB),触发Humongous分配,进而导致Full GC
GC日志关键片段:
1 | |
6.3 调优方案
1 | |
6.4 调优效果
| 指标 | 调优前 | 调优后 | 改善 |
|---|---|---|---|
| Young GC暂停 | 30-50ms | 20-35ms | 30% |
| Mixed GC暂停 | 500-800ms | 80-120ms | 85% |
| Full GC频率 | 2-3次/小时 | 0次 | 100% |
| P99延迟 | 2000ms+ | 200ms | 90% |
6.5 进阶方案:迁移到ZGC
对于延迟敏感型应用,如果使用JDK 17+,可以进一步考虑迁移到ZGC:
1 | |
迁移到ZGC后,GC暂停时间稳定在亚毫秒级别,P99延迟降至50ms以内。
7. GC选型决策树
flowchart TB
Q1{应用类型?} -->|批处理/离线计算<br/>追求吞吐量| A1[Parallel GC]
Q1 -->|Web服务/微服务<br/>延迟敏感| Q2{JDK版本?}
Q1 -->|嵌入式/资源受限| A2[Serial GC]
Q2 -->|JDK 8| A3[CMS 或 G1]
Q2 -->|JDK 11-16| A4[G1 推荐]
Q2 -->|JDK 17+| Q3{延迟要求?}
Q3 -->|暂停<200ms即可| A5[G1]
Q3 -->|暂停<10ms| A6[ZGC 分代模式]
Q3 -->|暂停<1ms| A6
style A6 fill:#90EE90,stroke:#333,stroke-width:2px
style A4 fill:#87CEEB
style A5 fill:#87CEEB
8. 最佳实践
- 先测量再调优:不要凭感觉调优,先通过GC日志和监控数据确认问题所在。
- 设置合理的堆大小:
-Xms和-Xmx设为相同值,避免堆大小动态调整带来的开销。 - 关注Full GC:Full GC是性能杀手,出现Full GC通常意味着配置不合理或存在内存泄漏。
- 生产环境必须开启GC日志:GC日志对性能的影响微乎其微,但对问题排查至关重要。
- 配置OOM自动dump:
-XX:+HeapDumpOnOutOfMemoryError是生产环境标配。 - 逐步调优:每次只调整一个参数,观察效果后再调整下一个,避免多个参数互相干扰。
- 优先升级JDK:新版JDK的GC往往有显著改进,升级JDK本身就是一种调优。
9. 总结
GC调优是一个需要结合理论知识和实践经验的过程。核心思路如下:
- 了解各回收器的适用场景:没有万能的GC,只有最适合的GC
- G1是当前最通用的选择:适用于大多数场景,JDK 9+默认使用
- ZGC是未来的方向:如果使用JDK 17+且对延迟有极高要求,ZGC是最佳选择
- 调优的核心是减少STW暂停和Full GC:通过合理的内存分配、Region大小和暂停目标来实现
- 监控和日志是调优的基础:没有数据支撑的调优都是盲目的
$ echo "comments" · 评论