第 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 Stackjava
// 对象分配与晋升
// 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=1gtext
推荐配比
├── 堆:总内存 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=10java
// 查看默认 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=100mtext
# 常见 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.6msbash
# 实时监控 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 Treejava
// 典型 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=300java
// 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 |
动手练习
- GC 日志:启动应用,触发 Full GC,分析日志
- G1 调优:用 G1GC,调整
MaxGCPauseMillis,观察 GC 频率变化 - 内存泄漏:写一个有泄漏的 demo,导出 heap dump,用 MAT 分析
- 容器配比:在 K8s 中部署,设置内存 limit,验证 JVM 正确感知
下一章:第 23 章:SQL 调优 →