JVM 性能监控工具之命令行篇
一、为什么需要命令行监控工具在日常 Java 应用开发与运维工作中性能问题往往隐藏在运行时的内存、线程、垃圾回收和类加载等细节里。很多开发者习惯在本地 IDE 里打断点、查看变量但一旦应用部署到测试环境、生产环境IDE 调试手段基本失效尤其是当服务运行在无法提供图形界面的 Linux 服务器上时几乎只能依赖命令行工具完成问题定位。命令行监控工具之所以重要主要有以下四个原因。第一轻量且普适JDK 自带的命令行工具不依赖额外安装只要目标机器上有 JDK 或 JRE 即可使用适合任意服务器环境。第二信息真实它们直接读取 JVM 运行时的内部数据比如堆内存使用、GC 次数、线程状态、锁等待情况比外部监控系统更贴近虚拟机本身。第三适合自动化命令行输出可以方便地写入脚本、日志或监控平台配合定时采集实现持续监控。第四应急排查当线上出现 CPU 飙高、内存泄漏、线程死锁、频繁 Full GC 等问题时图形界面通常难以第一时间接入而命令行工具可以快速获取现场快照是问题定位的第一手材料。本文将以「JVM 命令行性能监控工具」为核心系统讲解 jps、jinfo、jstat、jmap、jstack、jcmd 等工具的原理、参数、输出解读和实战用法并结合线上故障排查流程帮助读者建立一套可落地的命令行诊断方法论。文章篇幅较长建议结合示例逐步实践。二、JVM 监控工具全景与前置准备2.1 工具家族概览在 JDK 的bin目录下与 JVM 监控和诊断相关的命令行工具主要分为两类一类是通用工具既能用于本地进程也能配合 JMX 或 Attach 机制连接远程进程另一类是深入 JVM 内部的专用工具。下表列出了最常用的几个工具及其核心职责。工具名称主要用途典型场景jps查看 Java 进程及其启动参数快速找到目标 JVM 的进程 IDjinfo查看和动态调整 JVM 配置参数确认参数是否生效、临时调整开关jstat监视 JVM 统计信息如 GC、类加载、编译持续观察 GC 频率、堆容量变化、类数量jmap输出堆内存信息、对象直方图、堆转储内存泄漏分析、对象分布统计jstack输出线程堆栈和锁信息CPU 飙高、线程死锁、线程阻塞分析jcmd功能聚合的命令行诊断工具一处查看线程、堆、GC、系统属性等信息此外还有jhat用于分析堆转储文件但由于它启动服务并解析 hprof 文件时内存消耗大、速度慢目前已很少在生产环境使用通常被 VisualVM、MAT、JProfiler 等可视化工具替代。本文重点讲解前面六个工具。2.2 使用前提与权限说明这些工具的底层大多依赖 JDK 的 Attach 机制。简单来说jps、jmap、jstack、jcmd、jinfo 需要以与目标 JVM 进程相同的操作系统用户身份运行否则会因为权限不足而无法访问目标进程。特别地如果目标进程运行在容器中还需要保证工具和应用位于同一个 PID 命名空间并正确设置/tmp目录下相关文件的访问权限。从 JDK 9 开始的模块化改造后部分工具如 jhat被移出默认发布包而 jcmd 的能力则进一步增强。建议在 JDK 8 至 JDK 21 环境中以 jcmd 为主、其他工具为辅进行诊断。本文示例主要以 Linux 环境下的 JDK 8 与 JDK 11 为主绝大多数命令在不同版本中保持兼容。2.3 一个贯穿全文的示例程序为了让后续命令输出更容易理解我们先准备一个简单的 Java 示例程序。它创建一定数量的对象启动多个线程模拟业务运行方便后续用各种工具观察其内存、线程和 GC 情况。import java.util.ArrayList; import java.util.List; public class JvmMonitorDemo { private static final Listbyte[] caches new ArrayList(); public static void main(String[] args) throws Exception { for (int i 0; i lt; 5; i) { Thread t new Thread(() -gt; { while (true) { caches.add(new byte[1024 * 1024]); try { Thread.sleep(50); } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } } }, worker- i); t.start(); } Thread.sleep(Long.MAX_VALUE); } }编译并运行该程序javac JvmMonitorDemo.java java -Xms128m -Xmx256m JvmMonitorDemo运行后程序会不断向caches列表中添加 1MB 的字节数组并启动 5 个工作线程。下面的所有示例都会基于这个程序展开便于读者在自己的环境中复现。三、jps快速查看 Java 进程3.1 jps 的作用与原理jps全称 Java Virtual Machine Process Status Tool用于列出当前系统中正在运行的 Java 进程并显示进程 ID。它是所有 JVM 诊断工作的第一步因为后续的 jstat、jmap、jstack 都需要目标进程的 PID 作为参数。jps 的输出类似 Linux 的ps命令区别在于 jps 只关心 Java 进程并且能够识别出 JVM 指定的一些附加信息例如启动类名或 jar 包名。jps 在查找进程时会扫描/tmp/hsperfdata_用户名目录下的性能数据文件因此如果目标进程以不同用户身份运行jps 默认看不到它。3.2 常用参数参数作用无参数显示进程 ID 和主类名或 jar 名-l显示完整的主类全限定名或 jar 完整路径-m显示传给 main 方法的参数-v显示 JVM 启动参数-q只显示进程 ID不显示类名3.3 示例与输出解读jps -lvm示例输出如下23012 JvmMonitorDemo 23108 sun.tools.jps.Jps -lvm -Dapplication.home/usr/lib/jvm/java-11-openjdk 22044 org.apache.catalina.startup.Bootstrap start输出中每一行由三部分组成PID、主类名或 jar 路径、启动参数。通过-l可以准确区分同名短类名通过-v可以确认堆大小、GC 收集器等参数是否按预期生效通过-m可以查看应用自身的业务参数。3.4 使用建议在日常运维中建议始终使用jps -lvm组合参数。它一次性输出进程 ID、完整类名、JVM 参数和 main 参数极大地减少反复查询的时间。如果排查的目标是通过java -jar启动的服务-l会显示完整 jar 路径便于从多个相似进程中唯一定位目标。需要注意的是jps 不能跨用户查找进程。如果发现应用启动了但 jps 看不到第一时间检查当前用户和应用运行用户是否一致以及/tmp/hsperfdata_*目录是否可访问。四、jinfo查看与动态调整 JVM 参数4.1 jinfo 的作用jinfo全称 Configuration Info for Java用于查看运行中 JVM 的启动参数、系统属性和部分可动态修改的 VM 标志。它解决的问题是当应用已经启动较长时间我们不确定某个参数比如堆大小、GC 类型、编码是否真的生效时可以从运行中的 JVM 直接读取确认。jinfo 支持两类参数一类是启动时固定下来的参数如sun.boot.library.path等只能读取不能修改另一类是标记为 manageable 的参数可以在运行时动态调整例如PrintGCDetails、HeapDumpOnOutOfMemoryError等。4.2 常用参数jinfo -flags pid # 查看 JVM 启动参数实际生效值 jinfo -sysprops pid # 查看 System.getProperties() 系统属性 jinfo -flag name pid # 查看某个 VM 标志的当前值 jinfo -flag [|-]name pid # 动态开启或关闭某个布尔型 VM 标志 jinfo -flag namevalue pid # 动态设置某个可管理 VM 标志的值4.3 示例确认参数是否生效假设应用以如下命令启动java -Xms128m -Xmx256m -XX:UseG1GC JvmMonitorDemo运行一段时间后我们想确认 G1 收集器是否真的生效可以执行jinfo -flag UseG1GC 23012输出为-XX:UseG1GC这说明 G1 收集器已开启。继续查看完整启动参数jinfo -flags 23012示例输出如下VM Flags: -XX:ConcGCThreads2 -XX:G1HeapRegionSize1048576 -XX:InitialHeapSize134217728 -XX:MaxHeapSize268435456 -XX:UseG1GC -XX:UseCompressedClassPointers -XX:UseCompressedOops ...4.4 动态调整参数的实战价值某些以-XX:Print开头的诊断参数只有在 JVM 启动时开启才生效但部分 manageable 参数可以在不重启应用的情况下临时开启或关闭这对「不想重启但需要立刻采集信息」的场景非常有用。例如动态开启类加载打印jinfo -flag PrintClassHistogram 23012 jinfo -flag PrintGCDetails 23012采集完毕后可以再关闭jinfo -flag -PrintClassHistogram 23012 jinfo -flag -PrintGCDetails 23012需要强调的是并非所有参数都允许动态修改。只有被 JVM 标记为 manageable 的参数才支持运行时设置其余参数会报错或无法设置。因此生产环境最重要的参数仍应在启动脚本中显式指定jinfo 动态调整主要作为临时排查手段。4.5 使用建议当怀疑「参数配置没有生效」「用了新的启动脚本但行为异常」时优先使用jinfo -flags直接读取运行中 JVM 的真实参数而不是凭启动脚本猜测。脚本里的变量替换、环境差异都可能导致最终生效值与预期不同。五、jstatJVM 统计信息监视利器5.1 jstat 的作用与原理jstat全称 JVM Statistics Monitoring Tool是命令行环境下最常用的实时监控工具。它可以持续输出 JVM 的堆内存、垃圾回收、类加载、即时编译等统计信息非常适合观察应用在压力测试或线上运行时的动态变化。jstat 的数据来源是 JVM 内部维护的性能计数器通过本地进程间通信读取开销很低适合长时间、高频次采集。命令基本格式如下jstat -option pid [interval_ms] [count]其中interval_ms是采样间隔单位毫秒count是采样次数。如果不指定则只输出一次。5.2 常用统计选项jstat 的选项非常多最常用的集中在类加载、编译和垃圾回收三个方面。下表列出核心选项。选项含义-class类加载、卸载数量及耗时-compilerJIT 编译信息如编译任务数、耗时-gc各代容量及 GC 总体统计-gccapacity各代容量及其上下限-gcutil各代使用占比及 GC 时间占比最常用-gccause-gcutil 信息外加最近一次 GC 原因-gcnew新生代详细统计-gcold老年代及元空间统计5.3 -gcutil最直观的 GC 监控-gcutil输出各内存区域的使用百分比以及 GC 累计时间占比非常适合快速判断堆压力。命令如下jstat -gcutil 23012 1000 5表示每 1000 毫秒输出一次共输出 5 次。示例输出S0 S1 E O M CCS YGC YGCT FGC FGCT GCT 0.00 12.35 78.12 23.44 92.10 88.35 18 0.271 2 0.118 0.389 0.00 12.35 82.01 25.17 92.10 88.35 18 0.271 2 0.118 0.389 0.00 12.35 85.90 26.89 92.11 88.35 19 0.286 2 0.118 0.404 0.00 12.35 89.15 28.44 92.11 88.35 19 0.286 2 0.118 0.404 0.00 12.35 93.70 30.18 92.11 88.35 20 0.301 2 0.118 0.419各列含义如下S0、S1Survivor 0 区和 Survivor 1 区的使用百分比。EEden 区使用百分比是新生代最活跃的区域。O老年代使用百分比持续增长往往意味着对象不断晋升。M元空间使用百分比。CCS压缩类空间使用百分比。YGC、YGCTYoung GC 次数和累计耗时秒。FGC、FGCTFull GC 次数和累计耗时秒。GCT所有 GC 累计耗时秒。从输出可以看到Eden 区使用率从 78% 增长到 93%并在接近阈值时触发了 Young GCYGC 从 18 增加到 20这是完全正常的新生代回收行为。如果观察到老年代使用率持续上升且 Young GC 后没有明显下降就需要警惕内存泄漏或晋升过快的问题。5.4 -gccause结合 GC 原因判断-gccause在-gcutil的基础上增加了两列LGCC和GCC分别表示上次 GC 的原因和当前 GC 原因。原因字符串如Allocation Failure、G1 Evacuation Pause、Metadata GC Threshold、Ergonomics等能帮助我们理解 JVM 为什么触发 GC。jstat -gccause 23012 2000 3示例输出S0 S1 E O M CCS YGC YGCT FGC FGCT GCT LGCC GCC 0.00 12.35 25.10 30.18 92.11 88.35 25 0.377 2 0.118 0.495 Allocation Failure Allocation Failure 0.00 12.35 48.22 30.18 92.11 88.35 25 0.377 2 0.118 0.495 Allocation Failure Allocation Failure 0.00 12.35 71.90 30.18 92.11 88.35 25 0.377 2 0.118 0.495 Allocation Failure Allocation Failure这里LGCC和GCC均为Allocation Failure表示 Young GC 是由于新生代分配空间不足而触发属于正常现象。5.5 -gccapacity查看各代容量配置有时我们不仅关心使用率还想知道每个区域的最小、最大容量以及当前容量。这时使用-gccapacity更合适。jstat -gccapacity 23012输出示例NGCMN NGCMX NGC S0C S1C EC OGCMN OGCMX OGC OC MCMN MCMX MC CCSMN CCSMX CCSC YGC FGC 21248.0 262656.0 56320.0 5632.0 5632.0 45056.0 45568.0 525312.0 45568.0 45568.0 0.0 1100800.0 43008.0 0.0 1048576.0 6656.0 22 2其中NGCMN/NGCMX/NGC分别表示新生代最小容量、最大容量和当前容量OGCMN/OGCMX/OGC对应老年代MCMN/MCMX/MC对应元空间。通过对比当前容量与最大容量可以判断 JVM 是否已经为后续对象分配预留了足够空间。5.6 -class 与 -compiler当怀疑应用存在大量动态类加载或者 ClassLoader 泄漏时可以用-class观察类数量是否持续增长。jstat -class 23012 2000 3示例输出Loaded Bytes Unloaded Bytes Time 6123 11423.6 88 92.0 8.31 6123 11423.6 88 92.0 8.31 6123 11423.6 88 92.0 8.31如果Loaded持续快速上升而Unloaded几乎不变说明类不断被加载却很少被卸载要检查是否存在热部署、动态代理或自定义 ClassLoader 泄漏。-compiler用于查看 JIT 编译情况输出中Compiled表示编译任务数Failed表示失败数。在应用刚启动时编译任务会快速增加预热完成后趋于平稳。5.7 jstat 实战观察技巧把 jstat 作为持续监控工具时建议使用如下组合命令每隔 2 秒采集一次并追加写入日志便于事后离线分析jstat -gcutil 23012 2000 /var/log/jvm-gc.log观察日志时重点关注三个信号一是FGC是否频繁增长二是FGCT单次 Full GC 耗时是否过长三是 Full GC 后老年代使用率能否显著下降。如果 Full GC 频繁且回收后老年代依然居高不下基本可以判定存在内存泄漏或堆配置过小。六、jmap堆内存映像与对象统计6.1 jmap 的作用jmap全称 Memory Map用于查看 JVM 堆内存的详细信息并可以导出堆转储文件heap dump供离线分析。当应用出现内存占用持续增长、OOM 或对象分布异常时jmap 是获取内存证据的核心工具。jmap 最常用的三个功能是查看堆配置概要、统计各类型对象的数量和占用空间、导出二进制的 hprof 堆转储文件。6.2 常用参数参数作用-heap显示堆配置、各代容量和使用情况-histo输出堆中各类对象的数量和字节数直方图-histo:live执行 Full GC 后统计只保留存活对象-dump:formatb,filexxx.hprof导出二进制堆转储文件-clstats查看类加载器统计信息-finalizerinfo查看等待 finalization 的对象6.3 -heap查看堆配置概要jmap -heap 23012示例输出节选Attaching to process ID 23012, please wait... Debugger attached successfully. Server compiler detected. JVM version is 11.0.1810 using thread-local object allocation. Garbage-First (G1) GC with 4 thread(s) Heap Configuration: MinHeapFreeRatio 40 MaxHeapFreeRatio 70 MaxHeapSize 268435456 (256.0MB) NewSize 1363144 (1.299MB) MaxNewSize 160432128 (153.0MB) OldSize 5452592 (5.199MB) Heap Usage: G1 Heap: regions 256 capacity 268435456 (256.0MB) used 84305520 (80.4MB) free 184129936 (175.6MB) 31.4% used从输出可以快速了解堆的最大容量、当前使用量和使用率。在 G1 收集器下堆被划分为多个 region信息以 region 总数和 region 容量来呈现。6.4 -histo对象分布直方图-histo可以输出堆中每种类型的对象实例数量和占用字节数按占用空间从大到小排序。它是快速找出「哪些对象占用了大量内存」的最直接手段。jmap -histo 23012 | head -30示例输出num #instances #bytes class name ---------------------------------------------- 1: 1920 160419840 [B 2: 15422 2961024 [C 3: 2043 1291432 [I 4: 15420 370080 java.lang.String 5: 12970 311280 java.lang.Class 6: 8120 259840 java.util.HashMap$Node ...各列含义num是排名#instances是对象实例数量#bytes是总字节数class name是类名。其中[B表示 byte 数组[C表示 char 数组[I表示 int 数组。在上面的示例中[Bbyte 数组占据了约 160MB这正是示例程序不断添加 1MB 字节数组造成的。如果在真实应用中看到某个业务对象或集合类占用异常就可以顺着类名定位到创建这些对象的代码。6.5 -histo:live 与 finalizer 检查-histo:live会在统计前触发一次 Full GC只统计存活对象。这个操作在目标堆较大时会暂停应用因此生产环境要谨慎执行尽量在业务低峰期进行。它的好处是排除垃圾对象干扰让统计结果更接近真实内存占用。jmap -histo:live 23012 | head -20另外如果应用中有对象重写了finalize()方法且迟迟没有被回收可以通过-finalizerinfo查看等待终结的对象排查 finalize 方法执行过慢或对象泄漏问题。6.6 -dump导出堆转储文件当需要离线深度分析内存泄漏时必须导出完整的堆转储文件。最常用的命令格式如下jmap -dump:live,formatb,file/data/dump/heap.hprof 23012参数说明live表示导出前先执行 Full GC只转储存活对象能够显著减小文件体积formatb表示二进制格式file指定输出路径。如果不加live转储文件会包含所有对象体积更大但信息更完整。导出的 hprof 文件可以用 VisualVM、Eclipse MAT、JProfiler 等工具打开进行支配树分析、内存泄漏报告生成、GC roots 追溯等深度分析。6.7 jmap 的替代方案与风险提示在 JDK 9 及以上版本中官方更推荐使用jcmd GC.heap_dump来替代jmap -dump两者效果一致但 jcmd 是后续统一演进的主入口。此外jmap -F强制模式会通过进程内注入方式执行在某些 JDK 版本或特殊场景下可能与应用状态不一致应尽量避免使用优先通过正常 Attach 方式执行。无论如何jmap 在导出大堆时会带来较明显的停顿尤其-histo:live和-dump:live会触发 Full GC。生产环境操作前务必确认对业务的影响并在低峰期执行。七、jstack线程堆栈与死锁分析7.1 jstack 的作用jstack全称 Stack Trace用于打印目标 JVM 中所有线程的当前堆栈信息包括线程状态、调用栈、锁持有与等待关系。它是诊断 CPU 占用过高、线程阻塞、线程死锁、线程数量异常等问题的最重要工具。线程堆栈本质上是 JVM 在某一时刻对所有线程瞬时状态的快照。通过分析堆栈可以知道每个线程正在执行哪段代码、在等待哪个锁、被哪些线程阻塞从而还原并发场景下的事件脉络。7.2 基本用法jstack pid jstack -l pid # 输出额外的锁信息 jstack -F pid # 强制输出用于进程无响应时建议始终将输出保存到文件便于反复查看和搜索jstack 23012 /data/dump/thread.txt7.3 线程快照结构解读jstack 输出以线程为单位组织每个线程包含线程名、优先级、守护线程标记、线程 ID十六进制和十进制、线程状态以及调用栈。示例如下worker-0 #12 prio5 os_prio0 tid0x00007f2a8c001800 nid0x1b3c runnable [0x00007f2a6c5fb000] java.lang.Thread.State: RUNNABLE at java.util.ArrayList.add(ArrayList.java:463) at JvmMonitorDemo.lambda$main$0(JvmMonitorDemo.java:10) at JvmMonitorDemo$$Lambda$1/764977973.run(Unknown Source) at java.lang.Thread.run(Thread.java:748) Locked ownable synchronizers: - None关键字段解释如下线程名如worker-0由程序命名是定位业务线程的关键。tidJVM 内部线程唯一标识。nid操作系统原生线程 ID十六进制。这是与top -H -p pid输出的线程 ID 关联的关键字段用于定位 CPU 占用高的线程。线程状态如 RUNNABLE、BLOCKED、WAITING、TIMED_WAITING 等。调用栈从线程当前执行点向上回溯到Thread.run的方法调用链。7.4 定位 CPU 飙高的线程这是 jstack 最经典的实战用法之一。当某个 Java 进程 CPU 使用率异常升高时可以按以下步骤定位到具体代码。第一步找到 CPU 占用高的进程 PID假设为 23012。第二步查看该进程内各线程的 CPU 占用top -H -p 23012在 top 的交互界面中按 CPU 排序可按大写 P找到占用最高的线程记下其 TID例如6980。第三步把十进制 TID 转换为十六进制printf %x\n 6980得到十六进制值例如1b44。第四步在 jstack 输出中搜索该 nidjstack 23012 | grep -A 20 0x1b44或者更准确地在输出中查找nid0x1b44jstack 23012 thread.txt grep -A 30 nid0x1b44 thread.txt这样就能找到该高 CPU 线程正在执行的方法调用链进而定位是业务死循环、密集计算还是其他问题。7.5 线程死锁分析死锁是并发编程中的典型问题。jstack 在输出末尾会直接给出死锁检测结果。当一个线程持有锁 A 并等待锁 B而另一个线程持有锁 B 并等待锁 A 时就会形成死锁。下面是一个死锁示例程序public class DeadLockDemo { private static final Object lockA new Object(); private static final Object lockB new Object(); public static void main(String[] args) { new Thread(() -gt; { synchronized (lockA) { sleepQuietly(100); synchronized (lockB) { System.out.println(thread1 got both); } } }, thread1).start(); new Thread(() -amp;gt; { synchronized (lockB) { sleepQuietly(100); synchronized (lockA) { System.out.println(thread2 got both); } } }, thread2).start(); } private static void sleepQuietly(long ms) { try { Thread.sleep(ms); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } }运行该程序后执行 jstack输出末尾会出现类似内容Found one Java-level deadlock: thread2: waiting to lock monitor 0x00007f2a8c004e28 (object 0x00000000d6f1a2a0, a java.lang.Object), which is held by thread1 thread1: waiting to lock monitor 0x00007f2a8c003e28 (object 0x00000000d6f1a2b0, a java.lang.Object), which is held by thread2 Java stack information for the threads listed above: thread2: at DeadLockDemo.lambda$main$1(DeadLockDemo.java:23) - waiting to lock 0x00000000d6f1a2a0 (a java.lang.Object) - locked 0x00000000d6f1a2b0 (a java.lang.Object) ... thread1: at DeadLockDemo.lambda$main$0(DeadLockDemo.java:13) - waiting to lock 0x00000000d6f1a2b0 (a java.lang.Object) - locked 0x00000000d6f1a2a0 (a java.lang.Object) ... Found 1 deadlock.jstack 明确标注了Found one Java-level deadlock并列出了互相等待的线程、对象和源码行号极大降低了死锁排查难度。7.6 线程数量与状态分布分析除了单个线程我们还可以从整体视角统计线程数量和状态分布。例如统计各种线程状态的个数grep java.lang.Thread.State thread.txt | sort | uniq -c | sort -nr可能得到如下结果58 java.lang.Thread.State: RUNNABLE 24 java.lang.Thread.State: WAITING (parking) 9 java.lang.Thread.State: TIMED_WAITING (sleeping) 3 java.lang.Thread.State: BLOCKED (on object monitor) 2 java.lang.Thread.State: TIMED_WAITING (on object monitor)如果发现大量线程处于 BLOCKED 状态可能说明存在锁竞争热点大量 WAITING 线程可能是线程池空闲线程或等待外部资源的线程。结合线程名和调用栈可以进一步判断是否正常。7.7 jstack 使用注意事项第一jstack 抓取快照本身会对目标进程产生短暂影响尤其在堆内存很大或线程很多时抓取过程可能需要数秒期间某些线程状态可能变化。因此单次快照只能作为线索必要时应在数秒内连续抓取多次对比分析线程状态是否稳定。第二对于极高频率或对时间敏感的问题单次快照可能错过关键瞬间这时可结合kill -3 pid让 JVM 自行把线程堆栈打印到标准输出日志中避免外部工具接入带来的干扰。第三jstack 输出中的nid与操作系统线程 ID 的对应关系是连接 top 和 jstack 的桥梁务必熟练掌握十六进制转换与搜索方法。八、jcmd一站式 JVM 诊断命令8.1 jcmd 的定位jcmd是 JDK 7 后期引入、并在 JDK 8 之后逐渐成为推荐首选的全能诊断工具。它把 jps、jinfo、jmap、jstack 等工具的大部分能力统一到一个入口中通过子命令方式进行调用。使用 jcmd 的好处是记忆负担小、命令体系统一而且官方后续新增的诊断能力大多优先在 jcmd 中提供。8.2 查看进程与可用命令不带参数运行 jcmd 时等价于 jps列出所有 Java 进程jcmd输出示例23012 JvmMonitorDemo 23108 jdk.jcmd/sun.tools.jcmd.JCmd查看某个进程支持哪些诊断命令jcmd 23012 help输出会列出该 JVM 版本下所有可用的子命令例如Thread.print、GC.class_histogram、GC.heap_dump、VM.flags、VM.system_properties等。8.3 常用子命令对照下表把 jcmd 子命令与旧工具功能对应起来方便读者迁移使用。jcmd 子命令等价/类似的旧工具能力说明VM.flagsjinfo -flags查看 JVM 启动参数VM.system_propertiesjinfo -sysprops查看系统属性VM.uptime无直接对应查看 JVM 运行时长VM.versionjava -version查看运行中 JVM 的版本Thread.printjstack打印所有线程堆栈GC.class_histogramjmap -histo输出对象类型直方图GC.heap_dumpjmap -dump导出堆转储文件GC.run无直接对应请求 JVM 执行一次 GCGC.heap_infojmap -heap查看堆信息8.4 常用命令示例查看线程堆栈并保存到文件jcmd 23012 Thread.print /data/dump/thread-jcmd.txt导出堆转储jcmd 23012 GC.heap_dump /data/dump/heap-jcmd.hprof查看对象直方图jcmd 23012 GC.class_histogram | head -30查看运行时长jcmd 23012 VM.uptime查看 JVM 启动参数jcmd 23012 VM.flags8.5 使用 jcmd 处理 native 线程当 JVM 进程几乎无响应常规 jstack 无法正常 Attach 时jcmd 也提供了Thread.print -l等带锁信息参数。与jstack -F相比jcmd 的行为通常更稳定是官方推荐的替代方案。在 JDK 11 及以上环境中建议优先使用jcmd Thread.print获取线程栈。8.6 jcmd 的扩展优势jcmd 不仅聚合传统功能还支持对运行中 JVM 执行一些控制类操作例如GC.run主动触发 GC、ManagementAgent.start动态开启 JMX 管理代理等。这些能力让 jcmd 在自动化运维脚本中非常实用。建议团队统一制定 jcmd 的使用规范减少多工具混杂带来的学习成本。九、实战线上故障排查完整流程9.1 故障一CPU 使用率持续 100%假设线上服务 CPU 使用率突然飙升到 100%排查步骤如下。第一步用top -c找到占用 CPU 最高的 Java 进程假设 PID 为 23012。第二步用top -H -p 23012查看该进程内各线程的 CPU 占用找到最高的几个线程 TID。第三步将 TID 转为十六进制并在 jstack 中搜索printf %x\n 6980 # 假设得到的十六进制为 1b44 jstat -gcutil 23012 1000 5 # 同时观察 GC 是否异常 jstack 23012 thread.txt grep -A 30 nid0x1b44 thread.txt如果发现多个线程都在执行同一段业务方法并且该方法存在死循环或高复杂度计算就可以定位根因。同时通过 jstat 观察 GC 频率排除是频繁 Full GC 导致的 CPU 占用假象。若是 GC 线程占用高则应转向堆配置与 GC 参数优化。9.2 故障二内存持续增长直至 OOM当监控显示堆内存使用率持续攀升并最终触发 OOM 时需要尽快留存现场。最佳实践是在 JVM 启动参数中预先配置-XX:HeapDumpOnOutOfMemoryError -XX:HeapDumpPath/data/dump -XX:OnOutOfMemoryErrorkill -3 %p这样 OOM 发生时 JVM 会自动生成堆转储文件并打印线程栈避免事后现场消失。如果未配置该参数需要在进程还存活时手动导出jmap -dump:live,formatb,file/data/dump/heap-before-oom.hprof 23012 # 或 jcmd 23012 GC.heap_dump /data/dump/heap-before-oom.hprof导出后配合线上实时观察jstat -gcutil 23012 2000 10 jmap -histo:live 23012 | head -30如果直方图显示某个业务对象或集合类持续占用大量内存就围绕该类型的创建路径检查代码。最后用 VisualVM 或 MAT 打开 hprof 文件定位到持有大量对象的具体引用链确认根因。9.3 故障三接口响应变慢疑似线程阻塞或死锁当接口响应时间明显变长首先要确认是 CPU 瓶颈、锁竞争还是外部依赖变慢。抓取线程堆栈是关键。jcmd 23012 Thread.print thread1.txt sleep 3 jcmd 23012 Thread.print thread2.txt连续抓取多次对比线程状态。如果大量业务线程始终 BLOCKED 在同一个锁或同一个方法上说明存在锁竞争热点如果 jstack 输出末尾提示 deadlock则按提示直接修复锁顺序。还要关注WAITING (parking)的线程它们通常在等待 Condition、CompletableFuture 或外部服务需要结合业务判断是否合理。同时可以通过jstat -gcutil观察是否因频繁 GC 拖慢服务。把线程状态、GC 情况、系统负载三者的时间线对齐能显著提升定位效率。9.4 故障四类元空间溢出或类加载异常在大量使用动态代理、CGLIB、反射或热部署的系统中元空间有时会先于堆耗尽快耗尽。观察命令jstat -class 23012 2000 5 jstat -gcutil 23012 2000 5 jcmd 23012 GC.class_histogram | head -20如果M元空间使用率持续逼近上限且Loaded类数量不断增长、Unloaded几乎为零则应检查自定义 ClassLoader 是否被正确回收。常见原因包括类加载器被静态引用持有、线程上下文类加载器未释放、动态生成的类未卸载等。9.5 建立标准化排查脚本为了在故障发生时快速采集现场建议提前准备一套标准化脚本把关键信息一次性打包。下面给出一个可供参考的 shell 脚本框架#!/usr/bin/env bash PID$1 OUT_DIR/data/dump/$(date %Y%m%d_%H%M%S) mkdir -p $OUT_DIR jcmd $PID VM.uptime $OUT_DIR/uptime.txt jcmd $PID VM.flags $OUT_DIR/flags.txt jcmd $PID VM.system_properties $OUT_DIR/sysprops.txt jstat -gcutil $PID 1000 3 $OUT_DIR/gcutil.txt jcmd $PID GC.class_histogram $OUT_DIR/histo.txt jcmd $PID Thread.print -l $OUT_DIR/threads.txt echo dump saved to $OUT_DIR保存为jvm_dump.sh后使用./jvm_dump.sh 23012即可在数秒内采集一份完整诊断快照。对于需要堆转储的严重内存问题再单独执行 GC.heap_dump避免无差别采集大文件。十、高频命令参数速查表为方便日常查阅本节把前面讲解中最常用的命令进行汇总。所有命令中的pid请替换为目标 Java 进程 ID。10.1 进程与参数jps -lvm # 查看进程、类名和参数 jinfo -flags pid # 查看 JVM 启动参数 jinfo -flag UseG1GC pid # 查看单个参数 jcmd pid VM.uptime # 查看 JVM 运行时长 jcmd pid VM.version # 查看 JVM 版本10.2 GC 与堆监控jstat -gcutil pid 1000 5 # 持续观察各代使用率 jstat -gccause pid 1000 5 # 附加 GC 原因 jstat -gccapacity pid # 查看各代容量 jstat -class pid 1000 5 # 观察类加载 jcmd pid GC.heap_info # 查看堆概要10.3 内存分析jmap -histo pid | head -30 # 对象直方图 jmap -histo:live pid | head -30 # 存活对象直方图 jcmd pid GC.class_histogram | head -30 # jcmd 方式直方图 jmap -dump:live,formatb,fileheap.hprof pid # 导出堆转储 jcmd pid GC.heap_dump heap.hprof # jcmd 方式导出堆转储10.4 线程分析jstack pid thread.txt # 打印线程堆栈 jstack -l pid thread-l.txt # 附带锁信息 jcmd pid Thread.print -l thread.txt # jcmd 方式打印线程 printf %x\n tid # 十进制线程 ID 转十六进制 grep -A 30 nid0xhex thread.txt # 查找特定线程10.5 JDK 版本差异速记在 JDK 8 时代jmap、jstack、jinfo 被广泛使用JDK 9 之后官方逐步引导开发者迁移到 jcmd。JDK 11 移除了部分如-XX:PrintGCDetails的传统参数并以统一日志参数-Xlog:gc*取代。JDK 17 及以上版本中jcmd 的能力更加完善且部分工具参数在模块化下受限。建议读者根据所使用的 JDK 版本优先选用 jcmd并用jcmd pid help查看当前版本实际支持的子命令。十一、总结与进阶建议11.1 命令行工具的组合使用原则单一工具只能提供某一维度的信息真正高效的诊断往往需要多个工具协同。推荐的组合思路是先用 jps 定位进程用 jinfo 确认配置用 jstat 观察趋势用 jstack 分析线程用 jmap 或 jcmd 采集内存证据。对于任何一次性能问题都应尽量同时采集线程、GC、内存和系统负载四个维度的数据避免只盯着单一指标下结论。11.2 生产环境安全第一所有可能影响性能的操作都必须谨慎。jmap -dump:live、-histo:live会触发 Full GC可能造成秒级甚至更久的停顿高频执行 jstat 虽然开销很小但也应控制频率导出堆转储文件时要检查磁盘空间避免因磁盘写满引发二次故障。建议所有高风险操作都在低峰期执行并提前与团队沟通。11.3 监控体系的分层建议命令行工具适合应急排查和短时观察但难以做到 7x24 小时的可视化监控。生产环境更合理的做法是分三层第一层用 Prometheus、Grafana、Zabbix 等平台监控 JVM 指标做长期趋势和告警第二层用 JDK 命令行工具在告警触发后快速获取现场第三层用 MAT、JProfiler、Arthas 等工具做深度离线分析。命令行工具在第二层扮演承上启下的关键角色。11.4 进阶学习方向掌握本文介绍的 jps、jinfo、jstat、jmap、jstack、jcmd 后可以继续深入学习以下方向一是 GC 日志的统一配置与解析理解不同收集器的工作机制二是堆转储的支配树与泄漏分析掌握 MAT 的 OQL 查询三是线程同步模型与锁优化理解 synchronized、Lock、CAS 在堆栈中的表现四是容器化环境下的 JVM 诊断处理 cgroup 限制、PID 命名空间、Attach 受限等新问题五是 Alibaba Arthas 等在线诊断工具在无法停机或无法导出文件时进行在线排查。命令行工具虽然外观朴素却是每一个资深 Java 工程师必备的基本功。理解它们的输出、熟悉它们的组合用法、尊重它们对生产环境的影响边界才能在关键时刻快速、稳妥地解决问题。建议读者把本文中的每个命令都在本地示例程序上亲手执行一遍并结合自己项目的真实场景反复练习逐步形成自己的诊断直觉。