Java · #tuning#jvm#java#gc#g1#zgc

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
2
3
4
5
6
7
8
9
10
# 堆内存设置
-Xms4g # 初始堆大小
-Xmx4g # 最大堆大小(建议与Xms相同,避免动态调整)
-Xmn1g # 年轻代大小(G1中不建议手动设置)

# GC日志(JDK 9+统一日志框架)
-Xlog:gc*:file=gc.log:time,level,tags:filecount=10,filesize=100m

# GC日志(JDK 8)
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log

4.2 G1调优参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 启用G1(JDK 9+默认)
-XX:+UseG1GC

# 最大暂停时间目标(默认200ms)
-XX:MaxGCPauseMillis=100

# 触发并发标记的堆占用比例(默认45%)
-XX:InitiatingHeapOccupancyPercent=40

# Region大小(1MB-32MB,必须是2的幂)
-XX:G1HeapRegionSize=8m

# 混合回收时老年代Region最大占比
-XX:G1MixedGCLiveThresholdPercent=85

# 并发标记线程数
-XX:ConcGCThreads=4

# GC工作线程数
-XX:ParallelGCThreads=8

# 字符串去重(节省内存)
-XX:+UseStringDeduplication

4.3 ZGC调优参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 启用ZGC
-XX:+UseZGC

# JDK 21+启用分代ZGC(推荐)
-XX:+UseZGC -XX:+ZGenerational

# 堆大小(ZGC建议给足内存)
-Xms8g -Xmx8g

# 并发GC线程数
-XX:ConcGCThreads=4

# 软引用清理策略(默认25)
-XX:SoftMaxHeapSize=6g

5. GC监控工具

5.1 jstat实时监控

1
2
3
4
5
6
7
8
9
10
11
# 每1秒输出一次GC统计,共输出10次
jstat -gcutil <pid> 1000 10

# 输出示例:
# S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
# 0.00 45.23 67.89 34.56 95.12 92.34 120 1.234 3 0.567 1.801
# S0/S1: Survivor区使用率 E: Eden使用率 O: 老年代使用率
# YGC/YGCT: Young GC次数/耗时 FGC/FGCT: Full GC次数/耗时

# 查看GC原因
jstat -gccause <pid> 1000

5.2 jmap堆分析

1
2
3
4
5
6
7
8
9
# 查看堆内存概况
jmap -heap <pid>

# 生成堆转储文件(会触发Full GC,生产环境慎用)
jmap -dump:format=b,file=heap.hprof <pid>

# 推荐:在OOM时自动生成heap dump
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/tmp/heap-dump.hprof

5.3 GC日志分析

推荐使用GCEasy(https://gceasy.io)或GCViewer等工具分析GC日志:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/**
* 模拟不同GC场景的测试程序
* 使用不同GC参数运行,对比GC日志
*/
public class GCSimulator {
private static final int MB = 1024 * 1024;
private static final java.util.List<byte[]> retainedObjects = new java.util.ArrayList<>();

public static void main(String[] args) throws InterruptedException {
System.out.println("GC Simulator started. PID: " + ProcessHandle.current().pid());

// 模拟短生命周期对象(触发Young GC)
for (int i = 0; i < 500; i++) {
byte[] shortLived = new byte[MB]; // 1MB临时对象
Thread.sleep(10);
}

// 模拟长生命周期对象晋升到老年代(触发Mixed/Old GC)
for (int i = 0; i < 200; i++) {
retainedObjects.add(new byte[MB]); // 持续占用内存
if (i % 50 == 0) {
System.out.println("Retained: " + retainedObjects.size() + " MB");
}
Thread.sleep(20);
}

// 释放部分老年代对象
for (int i = 0; i < 100; i++) {
retainedObjects.remove(0);
}

// 再次分配,触发Mixed GC
for (int i = 0; i < 100; i++) {
retainedObjects.add(new byte[MB]);
Thread.sleep(10);
}

System.out.println("Simulation complete.");
}
}

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日志分析,发现两个核心问题:

  1. Mixed GC暂停过长:每次Mixed GC回收过多Region,单次暂停达800ms
  2. 频繁Humongous分配:系统中存在大量4MB以上的报文对象,超过Region大小的一半(默认Region=4MB),触发Humongous分配,进而导致Full GC

GC日志关键片段:

1
2
3
4
5
6
[gc,start] GC(1234) Pause Full (G1 Humongous Allocation)
[gc,phases] GC(1234) Phase 1: Mark live objects 890.234ms
[gc,phases] GC(1234) Phase 2: Prepare for compaction 45.678ms
[gc,phases] GC(1234) Phase 3: Adjust pointers 234.567ms
[gc,phases] GC(1234) Phase 4: Compact heap 567.890ms
[gc] GC(1234) Pause Full (G1 Humongous Allocation) 7168M->4096M(8192M) 1738.369ms

6.3 调优方案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 调优前
-Xms8g -Xmx8g -XX:+UseG1GC

# 调优后
-Xms8g -Xmx8g
-XX:+UseG1GC
-XX:G1HeapRegionSize=16m # 增大Region到16MB,避免4MB对象成为Humongous
-XX:MaxGCPauseMillis=100 # 降低暂停目标到100ms
-XX:InitiatingHeapOccupancyPercent=35 # 提前触发并发标记,避免堆占满
-XX:G1MixedGCCountTarget=16 # 分散Mixed GC回收压力
-XX:ParallelGCThreads=8 # GC并行线程数=CPU核数
-XX:ConcGCThreads=2 # 并发标记线程数=ParallelGCThreads/4
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/tmp/heap-dump.hprof
-Xlog:gc*:file=/var/log/app/gc.log:time,level,tags:filecount=10,filesize=100m

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
2
3
4
5
6
# ZGC配置(JDK 17)
-Xms12g -Xmx12g # ZGC需要更多内存余量
-XX:+UseZGC
-XX:ConcGCThreads=2
-XX:SoftMaxHeapSize=10g # 软上限,GC会尽量在此范围内回收
-Xlog:gc*:file=/var/log/app/gc.log:time,level,tags:filecount=10,filesize=100m

迁移到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. 最佳实践

  1. 先测量再调优:不要凭感觉调优,先通过GC日志和监控数据确认问题所在。
  2. 设置合理的堆大小-Xms-Xmx设为相同值,避免堆大小动态调整带来的开销。
  3. 关注Full GC:Full GC是性能杀手,出现Full GC通常意味着配置不合理或存在内存泄漏。
  4. 生产环境必须开启GC日志:GC日志对性能的影响微乎其微,但对问题排查至关重要。
  5. 配置OOM自动dump-XX:+HeapDumpOnOutOfMemoryError是生产环境标配。
  6. 逐步调优:每次只调整一个参数,观察效果后再调整下一个,避免多个参数互相干扰。
  7. 优先升级JDK:新版JDK的GC往往有显著改进,升级JDK本身就是一种调优。

9. 总结

GC调优是一个需要结合理论知识和实践经验的过程。核心思路如下:

  • 了解各回收器的适用场景:没有万能的GC,只有最适合的GC
  • G1是当前最通用的选择:适用于大多数场景,JDK 9+默认使用
  • ZGC是未来的方向:如果使用JDK 17+且对延迟有极高要求,ZGC是最佳选择
  • 调优的核心是减少STW暂停和Full GC:通过合理的内存分配、Region大小和暂停目标来实现
  • 监控和日志是调优的基础:没有数据支撑的调优都是盲目的
作者 · authorzt
发布 · date2024-11-20
篇幅 · length3.1k 字 · 8 min
许可 · licenseCC BY-SA 4.0
$ echo "comments" · 评论