Skip to content
第 22 章 架构 ⏱ 13 分钟阅读

第 22 章:JVM 调优 ​

学习目标 ​

  • 掌握 JVM 内存模型和 GC 原理
  • 学会 G1/ZGC 收集器的选型
  • 用 jstat/jmap 工具分析 GC
  • 避免内存泄漏、GC 频繁的坑

一、JVM 内存结构 ​

text
Heap(堆)
├── Young Gen
│   ├── Eden
│   └── Survivor (S0, S1)
└── Old Gen

Non-Heap
├── Metaspace(元数据,JDK 8+)
├── Code Cache(JIT 编译)
└── Thread Stack
java
// 对象分配与晋升
// 1. 新对象 → Eden
// 2. Eden 满 → Minor GC,存活对象 → Survivor
// 3. Survivor 存活 15 次(默认) → Old
// 4. Old 满 → Major GC / Full GC

二、堆内存配置 ​

bash
# 堆内存
-Xms4g            # 初始堆大小
-Xmx4g            # 最大堆大小(必须与 Xms 一致,避免扩容抖动)

# 新生代
-Xmn2g            # 新生代固定 2G
# 或
-XX:NewRatio=2    # Old:Young = 2:1

# 元空间
-XX:MetaspaceSize=256m
-XX:MaxMetaspaceSize=512m

# 直接内存(NIO)
-XX:MaxDirectMemorySize=1g
text
推荐配比
├── 堆:总内存 50-70%
├── 新生代:堆的 1/3 - 1/2
└── 元空间:256m - 512m

⚠️ 坑 1:-Xmx 配得太大(>16G),GC 一次要几秒,STW 时间长。G1/ZGC 适合堆 > 8G。

三、GC 收集器选型 ​

text
GC 收集器对比
├── Serial: 单线程,适合客户端
├── Parallel: 多线程,吞吐量优先
├── CMS: 并发,已废弃
├── G1: 分区,延迟优先(JDK 9+ 默认)
└── ZGC: 超低延迟(< 1ms,JDK 11+)
bash
# G1(Garbage First)
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200          # 目标暂停 200ms
-XX:InitiatingHeapOccupancyPercent=45  # 触发并发标记 45%
-XX:G1HeapRegionSize=16m          # Region 大小(1-32m)

# ZGC(超低延迟)
-XX:+UseZGC
-XX:MaxGCPauseMillis=10
java
// 查看默认 GC
java -XX:+PrintCommandLineFlags -version
// openjdk version "17.0.13" 2024-10-15
// -XX:+UseG1GC

四、GC 日志分析 ​

bash
# JDK 9+ 统一日志
-Xlog:gc*:file=gc.log:time,uptime,level,tags:filecount=10,filesize=100m
text
# 常见 GC 日志
[2026-01-15T10:00:00.123+0800][info][gc] GC(1) Pause Young (Normal) 24M->8M(256M) 12.3ms
[2026-01-15T10:00:01.456+0800][info][gc] GC(2) Pause Young (Concurrent Start) 28M->10M(256M) 15.6ms
bash
# 实时监控 GC
jstat -gc <pid> 1s
# S0C S1C S0U S1U EC EU OC OU MC MU ...
# 256M 256M 0 100M 512M 200M 2048M 1500M ...

jstat -gcutil <pid> 1s
# S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
# 0.00 50.00 30.00 65.00 95.00 - 100 5.0 5 1.2 6.2

⚠️ 坑 2:Full GC 一次几秒,直接 STW 所有线程。1 小时超过 1 次 Full GC 就要排查。

五、GC 调优实战 ​

java
// 场景 1:Minor GC 频繁
// 现象:每秒 1 次 Minor GC
// 原因:新生代太小,对象很快晋升老年代
// 调优:-Xmn 调大,或减少短生命周期对象

// 场景 2:Full GC 频繁
// 现象:每 10 分钟 1 次 Full GC
// 原因:内存泄漏,或有大量大对象
// 排查:jmap heap dump → MAT 分析

// 场景 3:GC 后内存没降
// 现象:GC 后老年代占用 90% 一直高
// 原因:内存泄漏
// 排查:堆 dump,看对象引用链

六、内存泄漏排查 ​

bash
# 1. 导出堆 dump
jmap -dump:format=b,file=heap.hprof <pid>

# 2. 启动时自动 dump
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/var/log/heapdump.hprof

# 3. 用 Eclipse MAT 分析
# 重点关注:Leak Suspects → Dominator Tree
java
// 典型 1:ThreadLocal 未清理
public void handleRequest() {
    ThreadLocal<User> tl = new ThreadLocal<>();
    tl.set(user);
    // ❌ 忘记 remove
}

// 修复:try-finally
try {
    tl.set(user);
    // 业务
} finally {
    tl.remove();
}
java
// 典型 2:监听器未注销
public void register() {
    EventBus.register(this);
    // ❌ 监听器持有外部引用
}

// 修复:显式注销
public void unregister() {
    EventBus.unregister(this);
}
java
// 典型 3:连接未关闭
public void query() {
    Connection conn = dataSource.getConnection();
    PreparedStatement ps = conn.prepareStatement("SELECT ...");
    // ❌ 异常时 connection 没释放
    ResultSet rs = ps.executeQuery();
    rs.close();
    ps.close();
    conn.close();
}

// 修复:try-with-resources
try (Connection conn = dataSource.getConnection();
     PreparedStatement ps = conn.prepareStatement("SELECT ...")) {
    // ...
}

七、JIT 优化 ​

bash
# 1. 分层编译(JDK 8+ 默认)
-XX:+TieredCompilation

# 2. 代码缓存
-XX:ReservedCodeCacheSize=256m
-XX:InitialCodeCacheSize=64m

# 3. 内联阈值
-XX:MaxInlineSize=35
-XX:MaxFreqInlineSize=300
java
// JIT 友好的代码
public int sum(int[] arr) {
    int sum = 0;
    for (int i = 0; i < arr.length; i++) {
        sum += arr[i];
    }
    return sum;
}
// 简单循环,JIT 会编译为机器码,接近 C 性能

八、线程栈 ​

bash
# 栈大小
-Xss512k           # 每个线程 512k

# 线程数 = (最大内存 - 堆 - 元空间) / 栈
# 例如 4G 内存,堆 2G,栈 1M
# 最多 2000 线程
java
// 线程池不是越多越好
ThreadPoolExecutor pool = new ThreadPoolExecutor(
    20, 100, 60, TimeUnit.SECONDS,
    new LinkedBlockingQueue<>(1000)
);
// 线程数 = CPU 密集型:CPU 核数;IO 密集型:CPU 核数 * 2

⚠️ 坑 3:线程池最大线程数配 1000,每次请求都创建新线程,内存爆炸。线程数应根据业务模型计算。

九、容器环境 JVM 调优 ​

bash
# 容器感知 JVM
# JDK 10+ 支持
-XX:+UseContainerSupport
-XX:MaxRAMPercentage=75.0        # 容器内存 75%

# Kubernetes 资源限制
resources:
  requests:
    memory: "4Gi"
    cpu: "2000m"
  limits:
    memory: "4Gi"
java
// 容器内 JVM 启动参数
java -XX:+UseContainerSupport \
     -XX:MaxRAMPercentage=75.0 \
     -XX:+UseG1GC \
     -XX:MaxGCPauseMillis=200 \
     -jar app.jar

本章小结 ​

收集器场景目标
G1通用,堆 4-32G延迟 200ms
ZGC堆 > 32G,超低延迟延迟 < 10ms
Parallel批处理吞吐量
关键点建议
堆大小4-8G,一致
GC 日志必开
OOM 自动 dump必开
容器MaxRAMPercentage

动手练习 ​

  1. GC 日志:启动应用,触发 Full GC,分析日志
  2. G1 调优:用 G1GC,调整 MaxGCPauseMillis,观察 GC 频率变化
  3. 内存泄漏:写一个有泄漏的 demo,导出 heap dump,用 MAT 分析
  4. 容器配比:在 K8s 中部署,设置内存 limit,验证 JVM 正确感知

下一章:第 23 章:SQL 调优 →

本站基于 VitePress 构建 · 由 StackHub 团队维护