摘要
JVM 调优的第一步是"看得见"——jstat 看内存趋势、jmap 做堆dump、jstack 抓线程快照、jcmd 统一诊断……这些 JDK 内置工具是每个 Java 工程师必须掌握的基本功。本文从实战角度讲解jstat/jmap/jstack/jcmd/VisualVM五大工具的核心用法:如何用 jstat 发现内存泄漏的苗头、如何用 jmap + MAT 分析 OOM 根因、如何用 jstack 定位死锁和 CPU 100% 问题、如何用 jcmd 一站式诊断。同时介绍Async-profiler和Arthas两个高级诊断工具,让你的问题定位效率提升 10 倍。
引言
“线上 CPU 飙到 100% 了,怎么办?”“内存一直增长,什么时候会 OOM?”“接口超时,怎么定位是 GC 导致的?”
这些问题都指向同一个能力:JVM 运行时监控与诊断。JDK 自带了一系列命令行工具,99% 的线上问题都可以用它们定位。问题是,大多数工程师只会jps查看进程 ID。
JVM 诊断工具矩阵: ┌─────────────────────────────────────────────────────────────────┐ │ JDK 内置诊断工具 │ ├────────────┬────────────────────────────────────────────────────┤ │ jps │ 查看 Java 进程 ID 和启动参数 │ │ jstat │ GC 统计、内存使用、类加载、编译信息 │ │ jmap │ 堆转储、内存分布、对象统计 │ │ jstack │ 线程快照、死锁检测、CPU 热点 │ │ jcmd │ 综合诊断工具(JDK 8+),统一 jmap/jstack 等 │ │ jinfo │ 运行时查看/修改 JVM 参数 │ ├────────────┴────────────────────────────────────────────────────┤ │ 高级诊断工具 │ ├────────────┬────────────────────────────────────────────────────┤ │ VisualVM │ 图形化 JVM 监控工具,支持插件扩展 │ │ async-profiler | 低开销采样分析器,支持 async-profiler │ │ Arthas │ 阿里开源:在线诊断、热修复、方法追踪 │ │ Java Flight Recorder | 商业级录制工具(OpenJDK 可用) │ └─────────────────────────────────────────────────────────────────┘一、jps:进程查看器
1.1 基本用法
# 查看所有 Java 进程jps-l# 输出:# 12345 com.example.Application# 12346 jar target/myapp.jar# 12347 sun.tools.jps.Jps# 查看详细启动参数jps-lv# 输出:# 12345 com.example.Application -Xms4g -Xmx4g -XX:+UseG1GC ...# 查看传递给 main 方法的参数jps-m1.2 实战场景
# 快速定位 Java 进程jps|grepmyapp# 结合 ps 查找psaux|grepjava|grep-vgrep# 查看所有 Java 相关进程(包括子进程)jps-m-l-v二、jstat:GC 统计神器
2.1 核心选项
# 语法jstat -<option><pid>[<interval>[<count>]]# 常用选项-gcutil# GC 覆盖率(百分比),最常用-gc# 各区域容量和使用量(字节)-gccapacity# 各区域容量(不显示使用量)-gcnew# 年轻代 GC 统计-gcold# 老年代 GC 统计-gcmetaspace# 元空间统计-class# 类加载统计-compiler# JIT 编译统计-printcompilation# 最近编译的方法2.2 GC 覆盖率(最常用)
# 每秒输出一次 GC 覆盖率,共输出 10 次jstat-gcutil12345100010# 输出:# S0 S1 E O M YGC YGCT FGC FGCT GCT# 12.50 0.00 65.00 78.50 85.20 123 4.567 12 0.890 5.457# 字段含义:# S0/S1: Survivor 0/1 使用率(%)# E: Eden 区使用率(%)# O: Old 区使用率(%)# M: Metaspace 使用率(%)# YGC: 年轻代 GC 总次数# YGCT: 年轻代 GC 总耗时(秒)# FGC: Full GC 总次数# FGCT: Full GC 总耗时(秒)# GCT: GC 总耗时(秒)┌──────────────────────────────────────────────────────────────────┐ │ jstat -gcutil 核心指标速读 │ ├──────────────────────────────────────────────────────────────────┤ │ │ │ 正常模式(锯齿波动): │ │ S0: 0% ───┐ │ │ E : 0% ──┤──→ 突然飙升 ──→ GC 后回落 │ │ O : 70% ─┘ (O 区稳定在 70% 是健康的) │ │ │ │ 危险模式(持续上升): │ │ O : 70% → 75% → 80% → 85% → ... │ │ ↑ 这是内存泄漏的早期信号! │ │ │ │ 告警阈值建议: │ │ - O > 80% 持续 5 分钟 → 关注,可能需要调优 │ │ - O > 90% → 接近 OOM,应立即介入 │ │ - M > 85% → 元空间增长,关注是否有大量类加载 │ │ │ └──────────────────────────────────────────────────────────────────┘2.3 内存详细统计
# 查看各区域详细容量和字节数jstat-gc12345# 输出:# S0C S1C S0U S1U EC EU OC OU# 349696.0 349696.0 43687.3 0.0 2797568.0 1832465.0 6997632.0 5497632.0# MC MU CCSC CCSU YGC YGCT FGC FGCT GCT# 5120.0 4352.0 768.0 640.0 123 4.567 12 0.890 5.457# 字段含义:# S0C/S1C: Survivor 0/1 容量(KB)# S0U/S1U: Survivor 0/1 已使用(KB)# EC/EU: Eden 容量/已使用(KB)# OC/OU: Old 容量/已使用(KB)# MC/MU: Metaspace 容量/已使用(KB)2.4 实战:监控内存趋势
# 监控脚本:检测内存泄漏#!/bin/bashPID=$1INTERVAL=5THRESHOLD=90whiletrue;doOLD_USAGE=$(jstat-gcutil$PID|tail-1|awk'{print $5}')echo"$(date'+%Y-%m-%d %H:%M:%S')Old:${OLD_USAGE}%"if(($(echo "$OLD_USAGE>$THRESHOLD"|bc-l)));thenecho"⚠️ WARNING: Old generation usage >${THRESHOLD}%"# 触发堆转储jmap-dump:format=b,file=heap_${PID}_$(date+%s).hprof$PIDfisleep$INTERVALdone三、jmap:堆内存诊断
3.1 堆直方图(快速定位)
# 查看堆中对象统计(按包名聚合)jmap-histo12345|head-50# 输出:# num #instances #bytes class name (module)# -------------------------------------------------------# 1: 12345 52428800 [Ljava.lang.Object;# 2: 8901 12345678 com.example.User# 3: 5678 9876543 com.example.Order# 4: 3456 2345678 java.lang.String# 5: 2345 1234567 java.util.HashMap$Node# 查找可疑的大对象jmap-histo12345|awk'$3 > 10000000 {print}'# 找出占用超过 10MB 的对象类型# 查看类加载器统计jmap-clstats123453.2 堆转储(核心技能)
# 生成堆转储文件(生产环境必用)# JDK 8 格式jmap-dump:format=b,file=heapdump.hprof12345# JDK 11+ 推荐:live 对象(只 dump 活着的对象,更小更快)jmap -dump:live,format=b,file=heapdump_live.hprof12345# JDK 11+:使用 jcmd(更推荐)jcmd12345GC.heap_dump heapdump.hprof jcmd12345GC.heap_dump-live=true heapdump_live.hprof# ⚠️ 注意:堆转储会 Stop-The-World,大堆(>8GB)可能暂停 30 秒以上# 建议:在低峰期执行,或使用 -XX:+HeapDumpOnOutOfMemoryError 自动触发3.3 堆内存分布
# 查看堆内存区域详细信息jmap-heap12345# 输出:# Heap Configuration:# MaxHeapSize = 4294967296 (4.0GB)# NewSize = 1073741824 (1.0GB)# MaxNewSize = 1073741824 (1.0GB)# OldSize = 3221225472 (3.0GB)# NewRatio = 3# SurvivorRatio = 8# MetaspaceSize = 268435456 (256.0MB)# ...## Heap Usage:# PS Young Generation# Eden Space:# capacity = 8589934592 (8.0GB)# used = 1234567890 (1.1GB)# free = 7355366602 (6.9GB)# 14.37% used# ...四、jstack:线程诊断
4.1 线程快照
# 生成线程快照jstack12345>threaddump.txt# 查找死锁jstack-l12345# 输出中的关键部分:# Found one Java-level deadlock:# =============================# "Thread-A":# waiting for ownable synchronizer 0x00007f1234567890,# (a java.util.concurrent.locks.ReentrantLock$NonfairSync)# which is held by "Thread-B"# "Thread-B":# waiting for ownable synchronizer 0x00007f12345678a0,# (a java.util.concurrent.locks.ReentrantLock$NonfairSync)# which is held by "Thread-A"4.2 CPU 100% 定位
# 步骤 1:找到占用 CPU 最高的线程top-Hp12345# 输出 PID 列表,找到占用 CPU 最高的线程 PID# 步骤 2:转换为十六进制printf'%x\n'12346# 假设输出:3032# 步骤 3:从 jstack 输出中查找该线程jstack12345|grep-A50"3032"# 输出示例:# "pool-1-thread-10" #12345 prio=5 os_prio=0 tid=0x00007f1234567890# java.lang.Thread.State: RUNNABLE# at com.example.Service.processLoop(Service.java:45)# at com.example.Service.run(Service.java:123)# at java.lang.Thread.run(Thread.java:750)# ↑# 根因代码位置!4.3 线程状态分析
jstack 线程状态速查: ┌──────────────────────────────────────────────────────────────────┐ │ 状态 │ 含义 │ ├─────────────────────┼──────────────────────────────────────────┤ │ RUNNABLE │ 正在运行(可能在计算,也可能在等待 I/O) │ │ BLOCKED │ 被阻塞,等待获取锁 │ │ WAITING │ 无限等待(Object.wait, LockSupport.park) │ │ TIMED_WAITING │ 限时等待(Thread.sleep, Lock.wait) │ │ NEW / TERMINATED │ 未启动 / 已结束 │ ├──────────────────────────────────────────────────────────────────┤ │ 危险信号: │ │ - 大量 RUNNABLE 且 CPU 高 → 死循环、密集计算 │ │ - 大量 BLOCKED → 锁竞争激烈 │ │ - 大量 WAITING → 线程池配置不当或任务饥饿 │ └──────────────────────────────────────────────────────────────────┘五、jcmd:统一诊断工具
JDK 8 引入的 jcmd 是诊断工具的"瑞士军刀":
# 查看可用命令jcmd12345help# 常用命令:jcmd12345GC.heap_dump /tmp/heap.hprof# 堆转储jcmd12345GC.run# 手动触发 Full GCjcmd12345GC.class_histogram# 堆直方图jcmd12345GC.class_stats# 类元数据统计(JDK 8u45+)jcmd12345VM.flags# 查看所有 JVM 参数jcmd12345VM.system_properties# 查看系统属性jcmd12345VM.version# JVM 版本信息jcmd12345Thread.print# 等同于 jstackjcmd12345VM.native_memory# 本地内存跟踪六、VisualVM:图形化监控
6.1 核心功能
# 启动 VisualVMjvisualvm# 或命令行指定端口连接远程 JVMjvisualvm--openjmxlocalhost:9999VisualVM 功能模块: ┌─────────────────────────────────────────────────────────────────┐ │ VisualVM 主界面 │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ 【监控】标签: │ │ - 堆使用量折线图(实时) │ │ - Metaspace 使用量 │ │ - 线程数 │ │ - 类加载数量 │ │ - CPU 使用率 │ │ │ │ 【线程】标签: │ │ - 线程状态时序图 │ │ - 死锁检测 │ │ - 线程堆栈查看 │ │ │ │ 【抽样器】标签: │ │ - CPU 采样(找出热点方法) │ │ - 内存采样(找出内存消耗大户) │ │ │ │ 【Profiler】标签(需要安装插件): │ │ - 更精确的性能分析 │ │ - 内存分配追踪 │ │ │ └─────────────────────────────────────────────────────────────────┘七、高级工具推荐
7.1 Async-profiler(推荐)
低开销的采样分析器,比 JProfiler 更轻量:
# 安装wgethttps://github.com/async-profiler/async-profiler/releases/download/v2.9/async-profiler-2.9-linux-x64.tar.gztar-xzfasync-profiler-2.9-linux-x64.tar.gz# CPU 采样(30 秒)./profiler.sh-d30-fcpu.html<pid># 内存分配采样./profiler.sh-d30-ealloc-falloc.html<pid># 锁竞争分析./profiler.sh-d30-elock-flock.html<pid>7.2 Arthas(阿里开源)
在线诊断神器,可以在不重启应用的情况下诊断问题:
# 启动java-jararthas-boot.jar# 常用命令dashboard# 查看 JVM 整体状态(仪表盘)thread# 查看线程状态thread-n3# 查看 CPU 最高的 3 个线程trace com.example.* run*'#cost > 100'# 追踪方法调用耗时watchcom.example.UserService getUser'{params, returnObj}'-x3monitor-c5com.example.* hello# 每 5 秒统计方法调用jad com.example.ClassName# 在线反编译总结
JVM 诊断工具是调优的"眼睛"。核心工具链:
- jstat -gcutil:每秒监控 GC 趋势,发现内存泄漏苗头
- jstack:抓线程快照,定位死锁和 CPU 100%
- jmap -dump:生成堆转储,用 MAT 分析 OOM 根因
- Arthas/async-profiler:在线诊断,追踪方法耗时和内存分配
系列导航
- 上一篇:【JVM深度解析】第11篇:GC日志配置与可视化分析
- 下一篇:【JVM深度解析】第13篇:生产环境JVM配置最佳实践
- 系列目录:JVM深度解析系列全集
参考资料
- Oracle JDK Tools Reference
- VisualVM Documentation
- Async-profiler GitHub
- Arthas Documentation
- MAT - Memory Analyzer Tool