一、引言
JVM 调优是 Java 开发者的必修课,但面对数百个 JVM 参数,很多人感到无从下手。
核心结论:JVM 调优不用背参数,记住 3 个原则就够了:
- 先监控再调优:不看 GC 日志就调参等于盲人摸象
- 代码优化优先:大多数性能问题是代码问题,不是参数问题
- 只调必要参数:堆大小、GC 算法、线程栈大小(极端场景)
二、原则 1:先监控再调优
2.1 不看 GC 日志就调参等于盲人摸象
错误做法:
┌─────────────────────────────────────────────────────┐
│ │
│ 服务器内存 8GB,直接设置 -Xmx6g -Xms6g │
│ 然后上线... │
│ │
│ 结果:可能导致频繁 Full GC,性能反而下降 │
│ │
└─────────────────────────────────────────────────────┘
正确做法:
┌─────────────────────────────────────────────────────┐
│ │
│ 1. 开启 GC 日志 │
│ 2. 运行一段时间(至少 24 小时) │
│ 3. 分析 GC 日志,了解堆使用情况 │
│ 4. 根据分析结果调整参数 │
│ │
└─────────────────────────────────────────────────────┘
2.2 开启 GC 日志
JDK 8 及之前
java -XX:+PrintGCDetails \
-XX:+PrintGCTimeStamps \
-XX:+PrintGCDateStamps \
-Xloggc:/var/log/app/gc.log \
-jar your-app.jar
JDK 9+(统一日志格式)
java -Xlog:gc*:file=/var/log/app/gc.log:time,uptime,level,tags \
-Xlog:safepoint*:file=/var/log/app/safepoint.log \
-jar your-app.jar
参数说明:
| 参数 | 说明 |
|---|---|
gc* | 输出所有 GC 相关日志 |
:file=xxx | 输出到文件 |
:time | 输出时间戳 |
:uptime | 输出 JVM 启动后的时间 |
:level | 输出日志级别 |
:tags | 输出日志标签 |
2.3 解读 GC 日志
JDK 8 GC 日志示例
2024-01-15T10:30:45.123+0800: [GC (Allocation Failure) [PSYoungGen: 1536M->256M(2048M)] 1536M->256M(6144M), 0.1234560 secs] [Times: user=0.34, sys=0.02, real=0.12 secs]
解读:
| 字段 | 说明 |
|---|---|
GC (Allocation Failure) | GC 类型(Minor GC,分配失败触发) |
PSYoungGen: 1536M->256M(2048M) | 年轻代使用量:1536M → 256M,总容量 2048M |
1536M->256M(6144M) | 堆使用量:1536M → 256M,总容量 6144M |
0.1234560 secs | GC 耗时 123ms |
JDK 9+ GC 日志示例
[10.304s][info][gc] GC(1) Pause Young (Allocation Failure) 1536M->256M(6144M) 123.456ms
[10.304s][info][gc] GC(1) Young Generation: 1536M->256M(2048M)
[10.304s][info][gc] GC(1) Survivor Space: 128M->0M(256M)
[10.304s][info][gc] GC(1) Old Generation: 0M->0M(4096M)
解读:
| 字段 | 说明 |
|---|---|
[10.304s] | JVM 启动后 10.304 秒 |
[info] | 日志级别 |
[gc] | 日志标签 |
GC(1) | 第 1 次 GC |
Pause Young | 年轻代暂停 |
1536M->256M(6144M) | 堆使用量变化 |
123.456ms | GC 耗时 |
2.4 关键指标
GC 日志关键指标:
┌─────────────────────────────────────────────────────┐
│ │
│ 1. Minor GC 频率 │
│ └── 理想:每 10-30 秒一次 │
│ │
│ 2. Minor GC 耗时 │
│ └── 理想:< 50ms │
│ │
│ 3. Full GC 频率 │
│ └── 理想:每小时 < 1 次 │
│ │
│ 4. Full GC 耗时 │
│ └── 理想:< 500ms │
│ │
│ 5. 堆使用率 │
│ └── 理想:60%-70% │
│ │
└─────────────────────────────────────────────────────┘
三、原则 2:代码优化优先
3.1 大多数性能问题是代码问题
性能问题分布:
┌─────────────────────────────────────────────────────┐
│ │
│ 代码问题:███████████████████████████░░░ 80% │
│ JVM 参数问题:██████████░░░░░░░░░░░░░░░░ 20% │
│ │
└─────────────────────────────────────────────────────┘
常见代码问题:
1. 内存泄漏(未关闭的资源、静态集合)
2. 慢 SQL(未加索引、全表扫描)
3. 同步瓶颈(不必要的锁)
4. 大对象分配(频繁创建大数组、字符串拼接)
3.2 用 Arthas 找到慢方法
安装 Arthas
curl -O https://arthas.aliyun.com/arthas-boot.jar
java -jar arthas-boot.jar
使用 trace 命令
# 追踪指定方法及其调用链的耗时
trace com.example.service.OrderService createOrder
# 结果示例:
`---ts=2024-01-15 10:30:45.123;thread_name=http-nio-8080-exec-1;id=1;is_daemon=true;priority=5;TCCL=org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader@12345678
`---[150.00ms] com.example.service.OrderService:createOrder()
+---[10.00ms] com.example.repository.OrderRepository:save()
+---[100.00ms] com.example.service.PaymentService:pay()
| `---[90.00ms] com.example.client.PaymentClient:callPaymentAPI()
+---[30.00ms] com.example.service.NotificationService:send()
+---[0.00ms] com.example.mapper.OrderMapper:toDTO()
解读:
| 字段 | 说明 |
|---|---|
[150.00ms] | 总耗时 150ms |
[100.00ms] | PaymentService.pay() 耗时 100ms(瓶颈) |
[90.00ms] | PaymentClient.callPaymentAPI() 耗时 90ms |
使用 watch 命令
# 观察方法的入参、出参和异常
watch com.example.service.OrderService createOrder "{params, returnObj, throwExp}" -x 2
# 结果示例:
method=com.example.service.OrderService.createOrder location=AtExit
ts=2024-01-15 10:30:45.123
params[0]=OrderCreateRequest(userId=1001, productId=2001, quantity=2)
returnObj=OrderDTO(orderId=3001, amount=99.98, status=CREATED)
throwExp=null
使用 profiler 命令
# 启动 CPU 分析,持续 60 秒
profiler start --event cpu
sleep 60
profiler stop --format html --file /tmp/cpu-profile.html
# 结果:生成火焰图,直观看到 CPU 热点
3.3 代码优化示例
问题 1:频繁创建大对象
// 优化前
public String generateReport(List<Order> orders) {
StringBuilder sb = new StringBuilder();
for (Order order : orders) {
// 每次循环创建新的 StringBuilder
sb.append(new StringBuilder()
.append("Order: ").append(order.getId())
.append(", Amount: ").append(order.getAmount())
.toString());
}
return sb.toString();
}
// 优化后
public String generateReport(List<Order> orders) {
StringBuilder sb = new StringBuilder();
for (Order order : orders) {
// 复用同一个 StringBuilder
sb.append("Order: ").append(order.getId())
.append(", Amount: ").append(order.getAmount());
}
return sb.toString();
}
问题 2:未关闭的资源
// 优化前
public String readFile(String path) throws IOException {
BufferedReader reader = new BufferedReader(new FileReader(path));
String content = reader.readLine();
// 忘记关闭 reader,可能导致资源泄漏
return content;
}
// 优化后
public String readFile(String path) throws IOException {
try (BufferedReader reader = new BufferedReader(new FileReader(path))) {
// try-with-resources 自动关闭资源
return reader.readLine();
}
}
四、原则 3:只调必要参数
4.1 参数调优优先级
参数调优优先级:
┌─────────────────────────────────────────────────────┐
│ │
│ 第一优先级:堆大小(-Xmx/-Xms) │
│ └── 决定了 GC 的频率和内存使用上限 │
│ │
│ 第二优先级:GC 算法(-XX:+UseG1GC/-XX:+UseZGC) │
│ └── 决定了 GC 的暂停时间和吞吐量 │
│ │
│ 第三优先级:其他参数(线程栈、元空间等) │
│ └── 一般不需要调,除非有特定问题 │
│ │
└─────────────────────────────────────────────────────┘
4.2 堆大小设置
基本公式
-Xmx = -Xms = 可用内存 * 0.7
注意事项
堆大小设置注意事项:
1. -Xmx 和 -Xms 设置为相同值,避免堆扩展开销
2. 不要超过物理内存的 80%,给系统留余量
3. 不要设置过大,否则 Full GC 耗时会很长
4. 根据 GC 日志调整,观察堆使用率是否在 60%-70%
示例
# 服务器内存 8GB,设置堆大小为 5GB
java -Xmx5g -Xms5g -jar your-app.jar
# 服务器内存 16GB,设置堆大小为 10GB
java -Xmx10g -Xms10g -jar your-app.jar
4.3 GC 算法选择
G1 GC(默认,JDK 9+)
适用场景:大多数企业级应用,堆大小 4GB-32GB
特点:
- 目标是控制 GC 暂停时间(默认 200ms)
- 分区收集,逐步清理
- 适合需要低延迟的场景
参数:
java -Xmx5g -Xms5g \
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=200 \
-XX:InitiatingHeapOccupancyPercent=70 \
-jar your-app.jar
| 参数 | 说明 |
|---|---|
-XX:+UseG1GC | 使用 G1 GC |
-XX:MaxGCPauseMillis=200 | 目标暂停时间 200ms |
-XX:InitiatingHeapOccupancyPercent=70 | 堆使用率达到 70% 时触发并发收集 |
ZGC(JDK 15+ 生产可用)
适用场景:大堆内存(32GB+),需要极低延迟
特点:
- 暂停时间极短(< 10ms)
- 并发压缩
- 适合堆内存较大的场景
参数:
# JDK 15+
java -Xmx32g -Xms32g \
-XX:+UseZGC \
-jar your-app.jar
# JDK 21+(分代 ZGC)
java -Xmx32g -Xms32g \
-XX:+UseZGC \
-XX:+ZGenerational \
-jar your-app.jar
| 参数 | 说明 |
|---|---|
-XX:+UseZGC | 使用 ZGC |
-XX:+ZGenerational | 使用分代 ZGC(JDK 21+) |
GC 算法对比
| 特性 | G1 GC | ZGC |
|---|---|---|
| JDK 版本 | JDK 7+(默认 JDK 9+) | JDK 15+(生产可用) |
| 暂停时间 | ~200ms | < 10ms |
| 堆大小 | 4GB-32GB | 8GB-16TB |
| 并发压缩 | 否 | 是 |
| 适用场景 | 大多数企业应用 | 大堆内存、低延迟要求 |
4.4 线程栈大小
默认值:1MB(JDK 8)
何时需要调整:
需要调整线程栈大小的场景:
1. 深度递归调用(栈溢出)→ 增大 -Xss
2. 大量线程(内存不足)→ 减小 -Xss
注意:调整栈大小风险较高,需要谨慎
示例:
# 增大栈大小(处理深度递归)
java -Xss2m -jar your-app.jar
# 减小栈大小(大量线程)
java -Xss256k -jar your-app.jar
五、各场景推荐参数速查表
5.1 小型服务(2GB 内存)
# 适合:单实例小型应用、开发环境
java -Xmx1536m -Xms1536m \
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=200 \
-Xlog:gc*:file=/var/log/app/gc.log:time,uptime \
-jar your-app.jar
5.2 中型服务(8GB 内存)
# 适合:微服务、中等流量应用
java -Xmx5g -Xms5g \
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=200 \
-XX:InitiatingHeapOccupancyPercent=70 \
-XX:ParallelGCThreads=4 \
-XX:ConcGCThreads=2 \
-Xlog:gc*:file=/var/log/app/gc.log:time,uptime,level,tags \
-jar your-app.jar
5.3 大型服务(16GB+ 内存,JDK 8/11)
# 适合:高并发应用、数据处理服务(JDK 8/11)
java -Xmx10g -Xms10g \
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=300 \
-XX:InitiatingHeapOccupancyPercent=70 \
-XX:ParallelGCThreads=8 \
-XX:ConcGCThreads=4 \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/var/log/app/heapdump.hprof \
-Xlog:gc*:file=/var/log/app/gc.log:time,uptime,level,tags \
-jar your-app.jar
5.4 大型服务(32GB+ 内存,JDK 15+)
# 适合:超大堆内存应用、极低延迟要求(JDK 15+)
java -Xmx24g -Xms24g \
-XX:+UseZGC \
-XX:+ZGenerational \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/var/log/app/heapdump.hprof \
-Xlog:gc*:file=/var/log/app/gc.log:time,uptime,level,tags \
-jar your-app.jar
5.5 大数据处理(128GB+ 内存)
# 适合:Spark、Flink 等大数据框架
java -Xmx96g -Xms96g \
-XX:+UseZGC \
-XX:+ZGenerational \
-XX:ParallelGCThreads=16 \
-XX:ConcGCThreads=8 \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/var/log/app/heapdump.hprof \
-Xlog:gc*:file=/var/log/app/gc.log:time,uptime,level,tags \
-jar your-app.jar
5.6 速查表汇总
| 场景 | 内存 | JDK 版本 | 推荐参数 |
|---|---|---|---|
| 小型服务 | 2GB | 任意 | -Xmx1536m -Xms1536m -XX:+UseG1GC |
| 中型服务 | 8GB | 任意 | -Xmx5g -Xms5g -XX:+UseG1GC -XX:MaxGCPauseMillis=200 |
| 大型服务 | 16GB+ | 8/11 | -Xmx10g -Xms10g -XX:+UseG1GC -XX:MaxGCPauseMillis=300 |
| 大型服务 | 32GB+ | 15+ | -Xmx24g -Xms24g -XX:+UseZGC -XX:+ZGenerational |
| 大数据处理 | 128GB+ | 15+ | -Xmx96g -Xms96g -XX:+UseZGC -XX:+ZGenerational |
六、总结
6.1 三个原则回顾
三个原则回顾:
┌─────────────────────────────────────────────────────┐
│ │
│ 原则 1:先监控再调优 │
│ ├── 开启 GC 日志 │
│ ├── 运行一段时间 │
│ └── 分析日志后再调整 │
│ │
│ 原则 2:代码优化优先 │
│ ├── 用 Arthas trace 找到慢方法 │
│ ├── 用 profiler 分析 CPU 热点 │
│ └── 修复代码问题比调参更有效 │
│ │
│ 原则 3:只调必要参数 │
│ ├── 堆大小:-Xmx/-Xms │
│ ├── GC 算法:G1(默认)或 ZGC(大堆) │
│ └── 线程栈:极端场景才需要调 │
│ │
└─────────────────────────────────────────────────────┘
6.2 调优流程
调优流程:
┌─────────────────────────────────────────────────────┐
│ │
│ 1. 开启 GC 日志 │
│ ↓ │
│ 2. 运行应用(至少 24 小时) │
│ ↓ │
│ 3. 分析 GC 日志 │
│ ├── Minor GC 频率和耗时 │
│ ├── Full GC 频率和耗时 │
│ └── 堆使用率 │
│ ↓ │
│ 4. 用 Arthas 分析代码性能 │
│ ├── trace 找到慢方法 │
│ ├── profiler 分析 CPU 热点 │
│ └── watch 观察方法入参出参 │
│ ↓ │
│ 5. 优化代码 │
│ ↓ │
│ 6. 调整 JVM 参数 │
│ ├── 堆大小 │
│ └── GC 算法 │
│ ↓ │
│ 7. 持续监控,迭代优化 │
│ │
└─────────────────────────────────────────────────────┘
6.3 常见误区
常见误区:
┌─────────────────────────────────────────────────────┐
│ │
│ ❌ 误区 1:把堆设置得越大越好 │
│ → 堆越大,Full GC 耗时越长 │
│ │
│ ❌ 误区 2:不看日志直接调参 │
│ → 盲目调参可能适得其反 │
│ │
│ ❌ 误区 3:忽视代码问题 │
│ → 80% 的性能问题是代码问题 │
│ │
│ ❌ 误区 4:追求最新的 GC 算法 │
│ → ZGC 不是万能的,G1 对于大多数场景足够好 │
│ │
└─────────────────────────────────────────────────────┘
💡 互动话题:你在项目中遇到过哪些 JVM 性能问题?是怎么解决的?欢迎在评论区分享!
