news 2026/9/3 20:24:19

JVM 性能监控工具之命令行篇

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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 的进程 ID
jinfo查看和动态调整 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 List<byte[]> 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 的参数,可以在运行时动态调整,例如PrintGCDetailsHeapDumpOnOutOfMemoryError等。

4.2 常用参数

jinfo -flags <pid> # 查看 JVM 启动参数(实际生效值) jinfo -sysprops <pid> # 查看 System.getProperties() 系统属性 jinfo -flag <name> <pid> # 查看某个 VM 标志的当前值 jinfo -flag [+|-]<name> <pid> # 动态开启或关闭某个布尔型 VM 标志 jinfo -flag <name>=<value> <pid> # 动态设置某个可管理 VM 标志的值

4.3 示例:确认参数是否生效

假设应用以如下命令启动:

java -Xms128m -Xmx256m -XX:+UseG1GC JvmMonitorDemo

运行一段时间后,我们想确认 G1 收集器是否真的生效,可以执行:

jinfo -flag UseG1GC 23012

输出为:

-XX:+UseG1GC

这说明 G1 收集器已开启。继续查看完整启动参数:

jinfo -flags 23012

示例输出如下:

VM Flags: -XX:ConcGCThreads=2 -XX:G1HeapRegionSize=1048576 -XX:InitialHeapSize=134217728 -XX:MaxHeapSize=268435456 -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 的真实参数,而不是凭启动脚本猜测。脚本里的变量替换、环境差异都可能导致最终生效值与预期不同。

五、jstat:JVM 统计信息监视利器

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、S1:Survivor 0 区和 Survivor 1 区的使用百分比。
  • E:Eden 区使用百分比,是新生代最活跃的区域。
  • O:老年代使用百分比,持续增长往往意味着对象不断晋升。
  • M:元空间使用百分比。
  • CCS:压缩类空间使用百分比。
  • YGC、YGCT:Young GC 次数和累计耗时(秒)。
  • FGC、FGCT:Full GC 次数和累计耗时(秒)。
  • GCT:所有 GC 累计耗时(秒)。

从输出可以看到,Eden 区使用率从 78% 增长到 93%,并在接近阈值时触发了 Young GC(YGC 从 18 增加到 20),这是完全正常的新生代回收行为。如果观察到老年代使用率持续上升且 Young GC 后没有明显下降,就需要警惕内存泄漏或晋升过快的问题。

5.4 -gccause:结合 GC 原因判断

-gccause-gcutil的基础上增加了两列:LGCCGCC,分别表示上次 GC 的原因和当前 GC 原因。原因字符串如Allocation FailureG1 Evacuation PauseMetadata GC ThresholdErgonomics等,能帮助我们理解 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

这里LGCCGCC均为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:format=b,file=xxx.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.18+10 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,format=b,file=/data/dump/heap.hprof 23012

参数说明:live表示导出前先执行 Full GC,只转储存活对象,能够显著减小文件体积;format=b表示二进制格式;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.txt

7.3 线程快照结构解读

jstack 输出以线程为单位组织,每个线程包含线程名、优先级、守护线程标记、线程 ID(十六进制和十进制)、线程状态以及调用栈。示例如下:

"worker-0" #12 prio=5 os_prio=0 tid=0x00007f2a8c001800 nid=0x1b3c 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,由程序命名,是定位业务线程的关键。
  • tid:JVM 内部线程唯一标识。
  • 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 输出中搜索该 nid:

jstack 23012 | grep -A 20 "0x1b44"

或者更准确地在输出中查找nid=0x1b44

jstack 23012 > thread.txt grep -A 30 "nid=0x1b44" 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.printGC.class_histogramGC.heap_dumpVM.flagsVM.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 执行一次 GC
GC.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.flags

8.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 "nid=0x1b44" thread.txt

如果发现多个线程都在执行同一段业务方法,并且该方法存在死循环或高复杂度计算,就可以定位根因。同时,通过 jstat 观察 GC 频率,排除是频繁 Full GC 导致的 CPU 占用假象。若是 GC 线程占用高,则应转向堆配置与 GC 参数优化。

9.2 故障二:内存持续增长直至 OOM

当监控显示堆内存使用率持续攀升并最终触发 OOM 时,需要尽快留存现场。最佳实践是在 JVM 启动参数中预先配置:

-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data/dump -XX:OnOutOfMemoryError="kill -3 %p"

这样 OOM 发生时 JVM 会自动生成堆转储文件,并打印线程栈,避免事后现场消失。如果未配置该参数,需要在进程还存活时手动导出:

jmap -dump:live,format=b,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,format=b,file=heap.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 "nid=0x<hex>" 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 工程师必备的基本功。理解它们的输出、熟悉它们的组合用法、尊重它们对生产环境的影响边界,才能在关键时刻快速、稳妥地解决问题。建议读者把本文中的每个命令都在本地示例程序上亲手执行一遍,并结合自己项目的真实场景反复练习,逐步形成自己的诊断直觉。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/3 20:18:26

Linux 内核高危漏洞解析:SCTP 协议 cookie‑AUTH 输入校验缺失风险

2026‑08‑26&#xff0c;NVD 披露 CVE‑2026‑74752 严重级别漏洞&#xff0c;CVSS 评分 9.8&#xff0c;属于 sctp 子系统输入校验缺失问题。该漏洞出现在 SCTP cookie AUTH 状态处理逻辑&#xff0c;远程攻击者可通过构造恶意 COOKIE_ECHO 报文实现校验绕过与内存破坏&#…

作者头像 李华
网站建设 2026/9/3 20:17:25

计算机单片机毕设实战-基于 STM32 单片机的智能输液报警与远程数据监控系统 基于 STM32 的步进电机驱动输液调速与体征监测装置研发(013806)

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于嵌入式单片机&#xff0c;Java、小程序技术领域和毕业项目实战 ✌️…

作者头像 李华
网站建设 2026/9/3 20:14:36

基于Unet+ResNet的腹部多脏器分割实战:从原理到部署

简介&#xff1a;本资源是一套面向医学图像分割初学者与进阶研究者的深度学习实战项目&#xff0c;聚焦腹部多脏器五类别精细分割任务&#xff0c;融合Unet架构与Resnet骨干网络&#xff0c;并集成多尺度训练、多类别输出适配等关键技术点&#xff0c;适用于AI医疗方向的课程设…

作者头像 李华
网站建设 2026/9/3 20:10:09

WrenAI:开源自然语言转SQL工具,降低数据分析技术门槛

今天来看一个在数据分析和BI领域值得关注的开源项目——Canner/WrenAI。这个项目专注于解决自然语言到SQL查询的转换问题&#xff0c;让非技术用户也能通过简单对话直接获取数据库中的业务洞察。WrenAI的核心价值在于它能够理解用户的自然语言问题&#xff0c;自动生成准确的SQ…

作者头像 李华
网站建设 2026/9/3 20:09:24

树莓派LinuxCNC硬件框架全解析:从实时内核到步进驱动

在实际机器控制项目中&#xff0c;树莓派与 LinuxCNC 的组合经常被用来搭建低成本数控系统。这套硬件框架并不复杂&#xff0c;但很容易被误解成“在树莓派上装一个软件&#xff0c;然后把 GPIO 接到步进驱动器上就能跑”。真正决定系统能否稳定加工的是硬件框架&#xff1a;实…

作者头像 李华