Appearance
第39课:JVM调优
🎯 学习目标
- 掌握 JVM 参数分类(堆/栈/收集器/GC日志/元空间)
- 掌握 GC 日志分析与排查工具(jstat/jstack/jmap/jcmd/Arthas)
- 理解 JIT 编译、逃逸分析对性能的影响
- 知道常见调优场景与原则
📖 一、JVM 参数分类
1. 堆内存
bash
java -Xms4g -Xmx4g -Xmn1g -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m App-Xms:初始堆(建议=Xmx,避免动态扩容)-Xmx:最大堆-Xmn:新生代大小-XX:MetaspaceSize/-XX:MaxMetaspaceSize:元空间(JDK8+,替代永久代,用本地内存)
2. 栈
-Xss256k:每线程栈大小(默认 1MB,深递归或线程多时调整)
3. 收集器
bash
-XX:+UseSerialGC # Serial(单线程,客户端)
-XX:+UseParallelGC # Parallel(吞吐量优先,JDK8默认)
-XX:+UseG1GC # G1(JDK9+默认,可预测停顿)
-XX:+UseZGC # ZGC(JDK11+,亚毫秒停顿)4. GC 日志
bash
# JDK 8
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log
# JDK 9+ 统一日志
-Xlog:gc*:file=gc.log:time,uptime,level,tags5. 故障诊断
bash
-XX:+HeapDumpOnOutOfMemoryError # OOM 自动堆转储
-XX:HeapDumpPath=/tmp/heapdump.hprof
-XX:ErrorFile=/tmp/hs_err_pid%p.log # 致命错误日志📖 二、JIT 编译与逃逸分析
1. 解释执行 + JIT
JVM 先解释执行字节码(启动快),热点代码(频繁执行)由 JIT(即时编译器) 编译成机器码缓存,后续直接执行机器码(快)。
- C1 编译器:客户端,快速编译,简单优化。
- C2 编译器:服务端,深度优化(逃逸分析、内联、循环展开)。
- 分层编译(默认):C1 先编译,足够热再 C2。
-XX:CompileThreshold=10000(方法调用次数阈值触发 JIT)。
2. 逃逸分析
JIT 分析对象是否"逃逸"出方法/线程:
- 未逃逸:对象可栈上分配(不进堆,免 GC)、标量替换(拆成基本类型)、锁消除(无竞争锁去掉)。
java
StringBuilder sb = new StringBuilder(); // sb 不逃逸 → 可能栈上分配,免 GC逃逸分析大幅减少堆压力和 GC。理解它有助于写"不逃逸"的代码(局部对象、不传出引用)。
📖 三、排查工具
1. 命令行(JDK 自带)
| 工具 | 用途 |
|---|---|
jps | 列出 Java 进程 |
jstat -gc <pid> 1s | 每秒看堆各代使用、GC 次数 |
jstack <pid> | 线程栈(排查死锁、卡住) |
jmap -heap <pid> | 堆配置和使用 |
jmap -dump:format=b,file=h.hprof <pid> | 堆转储 |
jcmd <pid> GC.heap_info | 堆信息 |
jinfo -flags <pid> | JVM 参数 |
2. 图形化/分析
- VisualVM:堆/线程/GC 可视化。
- MAT:堆转储分析,找内存泄漏(支配树、泄漏嫌疑)。
- Arthas(阿里):在线诊断,热更新、watch 方法、trace 耗时。
- JMH:微基准测试,避免手写 benchmark 误区。
3. GC 日志分析
- GCViewer / GCEasy(在线):分析 GC 日志,看停顿时间、吞吐量、Full GC 频率。
📖 四、常见调优场景
1. Full GC 频繁
排查:堆太小、内存泄漏、大对象、元空间不足、老年代膨胀。
jstat -gc看老年代增长趋势。- MAT 分析堆转储找大对象/泄漏。
- 调大堆或换 G1/ZGC。
2. 停顿过长(延迟敏感)
- 换低延迟收集器(G1 → ZGC)。
-XX:MaxGCPauseMillis=50设目标停顿(G1)。- 减少老年代对象(避免 Full GC)。
3. 吞吐量不足(计算密集)
- Parallel GC(吞吐优先)。
- 增大堆减少 GC 频率。
- JIT 优化(确保热点被编译)。
4. OOM
- 加
-XX:+HeapDumpOnOutOfMemoryError,分析堆转储。 - 查内存泄漏(static 集合、缓存、ThreadLocal)。
5. CPU 飙高
top -Hp <pid>找高 CPU 线程。jstack转十六进制线程 ID 定位代码。- 多为死循环、频繁 GC、锁竞争。
⚠️ 五、常见陷阱
陷阱1:盲目调参
未定位问题就改参数。应先用工具(jstat/jmap/MAT)分析,对症下药。
陷阱2:手写 benchmark 误区
用 System.currentTimeMillis 测微秒级、JIT 未预热、未隔离 GC 等。用 JMH 做微基准。
陷阱3:-Xms ≠ -Xmx 的开销
默认 -Xms 小,运行中堆动态扩容有停顿。生产建议相等。
陷阱4:忽略 JIT 预热
benchmark 前不预热,JIT 未编译热点,测的是解释执行慢速。JMH 自动预热。
陷阱5:堆越大不一定越好
大堆 GC 停顿长(扫描多)。低延迟用 G1/ZGC,而非盲目加大堆。
🆚 六、Java vs C / 调优原则
| 特性 | C | Java |
|---|---|---|
| 优化 | gcc -O2 编译期 | JIT 运行期 |
| 内存 | 手动 | GC |
| Profiling | perf/gdb | jstack/jmap/Arthas |
调优原则:
- 先测量再优化(用工具定位瓶颈)。
- 优先应用层优化(算法/数据结构),再 JVM 调参。
- 改一个参数测一次,避免多变量。
- 生产监控(GC 频率/停顿、内存、CPU)+ 告警。
对 C 程序员:Java 把 C 的编译期优化(-O2)延后到运行期(JIT),好处是能用运行时信息优化(如根据实际类型内联),代价是启动慢、需预热。GC 调优替代了 C 的手动内存管理调优。
💡 七、最佳实践
- -Xms = -Xmx,避免动态扩容。
- JDK 9+ 默认 G1,大堆低延迟用 ZGC。
- OOM 自动堆转储:
-XX:+HeapDumpOnOutOfMemoryError。 - 微基准用 JMH,别手写。
- 监控 GC:日志 + 工具,关注 Full GC 频率/停顿。
- 先分析后调参,对症下药。
📝 练习预告
完成 练习/Ex39_JVMTuning.java 中的 6 道题:
- JVM 参数获取(Runtime/ManagementFactory)
- GC 观察(System.gc + freeMemory)
- JIT 预热与性能
- 线程栈分析(jstack 思考)
- 堆转储与 MAT 思路
- 综合:模拟性能问题排查(CPU/内存)
完成后对比 答案/Sol39.java,查看逐行讲解与多解法。
🧭 八、调优工作流
JVM 调优的核心不是“背参数”,而是建立闭环。
完整流程:
text
1. 描述问题:CPU 高、内存高、响应慢、吞吐低、频繁 OOM。
2. 固定场景:请求量、数据量、部署规格、JDK 版本、容器限制。
3. 收集证据:GC 日志、线程 dump、heap dump、JFR、监控曲线。
4. 提出假设:泄漏、锁竞争、分配过快、SQL 慢、网络慢。
5. 做最小变更:一次只改一个变量。
6. 压测验证:同样流量、同样数据、同样机器规格。
7. 记录结论:参数、指标变化、回滚方式。不要跳过第 3 步。没有证据的调优,大多是在制造新问题。
📊 九、核心指标解释
调优时最常看的指标:
text
CPU 使用率:是否被计算、GC、锁竞争或系统调用占满。
Load Average:运行队列压力。
堆使用量:对象分配和存活情况。
GC 次数:Young、Mixed、Full 的频率。
GC 暂停:P95/P99 停顿是否影响接口。
线程数:是否无限创建线程。
阻塞线程数:锁、IO、连接池是否耗尽。
分配速率:单位时间创建对象的速度。
老年代 GC 后占用:判断泄漏和长期存活对象。指标之间要联动看:
text
CPU 高 + GC 次数高:可能是频繁 GC。
CPU 高 + GC 正常:可能是死循环、计算热点或锁自旋。
内存高 + GC 后下降:可能只是正常缓存或流量峰值。
内存高 + GC 后不下降:重点排查泄漏。
响应慢 + CPU 不高:常见于 IO、锁、连接池等待。🧪 十、场景一:内存泄漏排查
现象:
text
Old 区持续上涨。
Full GC 后回落很少。
最终 OOM。处理流程:
text
开启 OOM heap dump。
用 jstat 观察 GC 后 Old 区。
导出 heap dump。
用 MAT 打开 Dominator Tree。
查看 retained size 最大对象。
追踪 GC Roots 引用链。
定位静态集合、缓存、队列、ThreadLocal 或监听器。启动参数:
bash
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/data/dump
-Xlog:gc*:file=/data/logs/gc.log:time,uptime,level,tags判断标准:
text
如果对象数量随请求持续增加,且业务上不该长期存在,大概率泄漏。
如果对象被缓存持有,要检查容量、TTL、淘汰策略。
如果被 ThreadLocal 持有,要检查线程池线程是否长期存活。🧪 十一、场景二:CPU 飙高排查
处理流程:
text
1. top 找到 Java 进程 pid。
2. top -Hp pid 找到高 CPU 线程 tid。
3. 把 tid 转成十六进制。
4. jstack pid 输出线程栈。
5. 搜索十六进制 nid。
6. 定位正在运行的方法。Linux 示例:
bash
top -Hp 12345
printf "%x\n" 6789
jstack 12345 > thread.txt常见原因:
text
死循环。
正则灾难回溯。
频繁 GC。
JSON 序列化大对象。
锁竞争导致自旋。
加密、压缩、图片处理等 CPU 密集任务。如果线程栈一直变化,可能是正常高吞吐计算;如果长时间停在同一行,重点检查死循环或热点方法。
🧪 十二、场景三:响应慢排查
响应慢不一定是 JVM 参数问题。
排查顺序:
text
入口网关耗时。
应用线程池排队。
数据库连接池等待。
SQL 执行时间。
外部 HTTP/RPC 调用。
锁等待。
GC 暂停。
序列化/反序列化。
日志同步写入。工具选择:
text
Arthas trace:看方法调用链耗时。
Arthas watch:观察参数和返回值。
jstack:看线程等待点。
JFR:低开销记录 CPU、锁、GC、分配。
APM:跨服务链路追踪。结论要用耗时占比表达:
text
总耗时 800ms。
数据库查询 620ms。
外部 HTTP 120ms。
应用计算 30ms。
GC 无明显暂停。这比“感觉 JVM 慢”更可执行。
🐳 十三、容器环境注意事项
现代 Java 服务常跑在 Docker/Kubernetes 中,调优必须考虑容器限制。
关注点:
text
容器 memory limit 是否小于 JVM 看到的内存。
堆内存之外还有元空间、线程栈、直接内存、JIT code cache。
Xmx 不要等于容器总内存。
线程数越多,栈内存越大。
NIO/Netty 可能使用较多直接内存。示例预算:
text
容器限制:2GB
Java 堆:1200MB
元空间:256MB
直接内存:256MB
线程栈和其他:预留 300MBJDK 10+ 默认更好地识别容器限制,但仍建议显式配置关键参数。
🛠 十四、参数变更模板
每次调参都应该留下记录:
text
问题描述:
当前指标:
修改参数:
修改原因:
预期效果:
风险:
回滚方式:
验证方法:
上线时间:
观察窗口:
结论:示例:
text
问题描述:Young GC 每秒 5 次,P99 响应时间抖动。
修改参数:扩大新生代,保持 Xms=Xmx。
预期效果:降低 Young GC 频率。
风险:老年代空间变小,晋升失败风险增加。
验证方法:压测 30 分钟,对比 GC 日志和 P99。
回滚方式:恢复上一版 JVM 参数。🔍 十五、自测问题
text
为什么调优前必须先采集证据?
GC 后 Old 区持续上涨说明什么?
CPU 高时如何从线程 ID 定位 Java 代码?
响应慢时为什么不能只看 JVM?
容器内 Xmx 为什么不能等于 memory limit?
JFR、jstack、heap dump 分别适合排查什么问题?
为什么一次只改一个 JVM 参数?
如何证明一次调优真的有效?✅ 十六、掌握标准
学完本课后,应能做到:
text
能列出生产 JVM 的基础诊断参数。
能采集 GC 日志、线程 dump、heap dump。
能使用 jstat 判断 GC 趋势。
能用 jstack 定位高 CPU 线程。
能用 heap dump 分析内存泄漏方向。
能解释 JIT 预热对基准测试的影响。
能区分吞吐优先和延迟优先调优目标。
能在容器环境中预留堆外内存。
能写出可回滚的 JVM 参数变更记录。JVM 调优的成熟标志是能用数据证明问题、证明修改有效,并能安全回滚。
🎓 下一步
- 第40课:网络编程-Socket — TCP/UDP、BIO/NIO、客户端-服务器