JVM 调优与排障
hardJVM调优OOMGC日志jstackjmapMAT
JVM 调优不是靠猜——而是靠数据。本文介绍排查线上问题的工具链和思路,让你面对 OOM、GC 频繁、CPU 飙高时不慌。
常见线上问题
| 现象 | 可能原因 |
|---|---|
| CPU 100% | 死循环、频繁 Full GC、线程死锁 |
| OOM: Java heap space | 内存泄漏或堆太小 |
| OOM: Metaspace | 加载了太多类(动态代理、热部署) |
| OOM: Direct buffer memory | NIO 直接内存未释放 |
| 应用卡顿 | STW 过长、锁竞争 |
| 接口超时 | GC Stop-The-World、线程阻塞 |
排查工具链
命令行工具
| 工具 | 用途 | 典型命令 |
|---|---|---|
jps |
查看 Java 进程 | jps -lvm |
jstat |
GC 统计 | jstat -gcutil <pid> 1000 10 |
jinfo |
查看/修改 JVM 参数 | jinfo -flags <pid> |
jmap |
内存分析、Heap Dump | jmap -dump:format=b,file=heap.hprof <pid> |
jstack |
线程堆栈 | jstack <pid> |
jcmd |
综合工具 | jcmd <pid> VM.flags |
CPU 飙高排查步骤
# 1. 找到 CPU 最高的 Java 进程
top
# 2. 找到该进程中 CPU 最高的线程
top -H -p <pid>
# 3. 将线程 ID 转为十六进制
printf '%x\n' <tid>
# 4. 在 jstack 输出中搜索该线程
jstack <pid> | grep -A 30 '<hex_tid>'
这样就能定位到具体哪行代码在消耗 CPU。
内存泄漏排查
# 1. 查看堆内存使用情况
jstat -gcutil <pid> 1000
# 2. 看到老年代持续增长不释放?生成 Heap Dump
jmap -dump:live,format=b,file=heap.hprof <pid>
# 3. 用 MAT (Eclipse Memory Analyzer) 或 VisualVM 分析
# MAT 会自动生成 Leak Suspects 报告
jstat 输出解读
$ jstat -gcutil <pid> 1000
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
0.00 85.12 67.34 42.18 96.45 93.12 156 1.234 3 0.567 1.801
| 列 | 含义 |
|---|---|
| S0/S1 | Survivor 0/1 使用率 |
| E | Eden 使用率 |
| O | 老年代使用率 |
| M | 元空间使用率 |
| YGC/YGCT | Young GC 次数/总耗时 |
| FGC/FGCT | Full GC 次数/总耗时 |
关注点:
- O 持续增长 → 可能内存泄漏
- FGC 频率高 → 堆太小或有泄漏
- FGCT 单次耗时长 → 需要优化 GC 或减少老年代存活对象
GC 日志分析
开启 GC 日志
# JDK 8
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log
# JDK 9+(统一日志框架)
-Xlog:gc*:file=gc.log:time,level,tags
GC 日志示例(G1)
[2024-01-15T10:30:45.123+0800] GC(42) Pause Young (Normal)
(G1 Evacuation Pause) 256M->128M(512M) 15.234ms
解读:
- 第 42 次 GC,Young GC
- 堆使用从 256M 降到 128M,总堆 512M
- 暂停时间 15.234ms
推荐工具:GCEasy 在线分析 GC 日志,生成可视化报告。
常见 OOM 场景与解决
1. Java heap space
// 原因:对象创建太多或内存泄漏
List<byte[]> list = new ArrayList<>();
while (true) {
list.add(new byte[1024 * 1024]); // 不停分配,没有释放
}
排查:
- Heap Dump → MAT 分析 → 找到占用最多内存的对象
- 通过 GC Roots 引用链找到谁在持有这些对象
- 常见原因:未关闭的集合缓存、静态 Map 不断增长、连接池泄漏
2. Metaspace
// 原因:大量动态生成的类
// 常见于:反射、动态代理、Groovy/CGLIB、热部署
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(MyClass.class);
enhancer.setCallback((MethodInterceptor) (o, m, args, p) -> p.invokeSuper(o, args));
enhancer.create(); // 每次创建一个新的代理类
}
解决:
- 设置
-XX:MaxMetaspaceSize上限(防止耗尽系统内存) - 检查动态代理是否有缓存
- 检查热部署后旧类加载器是否被 GC
3. 线程相关
java.lang.OutOfMemoryError: unable to create new native thread
原因:线程太多,超过操作系统限制。
排查:
jstack看线程数量和状态- 检查是否有线程池配置不当(核心线程数太大、线程未回收)
- Linux 上查看
ulimit -u(最大用户进程数)
4. GC overhead limit exceeded
GC 花费超过 98% 的时间但只回收了不到 2% 的内存。本质上是内存泄漏或堆太小。
常用 JVM 参数
堆内存
-Xms4g # 初始堆大小
-Xmx4g # 最大堆大小(建议与 Xms 相同)
-Xmn2g # 新生代大小
-Xss512k # 线程栈大小
GC 选择
# JDK 8 推荐
-XX:+UseG1GC
# JDK 11+ 低延迟
-XX:+UseZGC # 或 -XX:+UseShenandoahGC
# G1 调优
-XX:MaxGCPauseMillis=200 # 目标暂停时间
-XX:G1HeapRegionSize=16m # Region 大小
诊断
-XX:+HeapDumpOnOutOfMemoryError # OOM 时自动 Dump
-XX:HeapDumpPath=/path/to/dump.hprof # Dump 文件路径
-XX:ErrorFile=/path/to/hs_err_%p.log # JVM 崩溃日志
调优基本思路
1. 确定目标
| 场景 | 优化目标 |
|---|---|
| Web 应用 | 低延迟(GC 暂停时间短) |
| 大数据处理 | 高吞吐量(GC 占用时间少) |
| 微服务 | 低内存占用 |
2. 选择 GC 收集器
| GC | 适用场景 | 暂停时间 |
|---|---|---|
| G1 | 通用(JDK 8+ 推荐) | 可控 |
| ZGC | 超低延迟(JDK 11+) | < 1ms |
| Shenandoah | 低延迟 | < 10ms |
| Parallel GC | 高吞吐量 | 较长 |
3. 调优顺序
1. 先保证代码质量(避免内存泄漏、不必要的对象创建)
2. 合理设置堆大小(Xms = Xmx,避免扩缩容)
3. 选择合适的 GC
4. 根据 GC 日志微调参数
5. 监控验证效果
绝大多数性能问题不是 JVM 的问题,而是代码的问题。JVM 调优是最后一步,不是第一步。
生产环境核心踩坑点
| 问题 | 答案要点 |
|---|---|
| 如何排查 CPU 飙高? | top → top -H → jstack 找到高 CPU 线程 |
| 如何排查内存泄漏? | jstat 观察 → jmap dump → MAT 分析 |
| 常见的 OOM 有哪些? | heap space / Metaspace / unable to create thread / GC overhead |
| 有哪些常用调优参数? | Xms/Xmx/Xmn/Xss/GC 选择/HeapDumpOnOOM |
| G1 和 ZGC 的区别? | G1 暂停可控;ZGC 亚毫秒暂停但只有 JDK 11+ |
| 调优的一般步骤? | 确定目标 → 分析数据 → 选择 GC → 调参 → 验证 |