![图片[1]-30 JVM性能调优必备:6大命令行监控诊断工具全解析-速优课](http://www.suyouke.com/wp-content/uploads/2026/07/doubao_img_2304x1728_20260722_174942-1-1024x768.png)
本文导读
在生产环境中,Java 应用经常会遇到各种性能问题:内存泄漏、CPU 飙升、死锁、GC 频繁…… 当问题发生时,能否快速定位根因,往往直接影响故障恢复的时间。JDK 自带了一系列强大的命令行诊断工具,掌握它们是每个高级 Java 工程师的必备技能。
本文将带你系统学习 JDK 中最常用的 6 个命令行监控诊断工具,包括它们的使用场景、核心参数和实战技巧。
通过阅读本文,你将了解:
- 如何快速定位 Java 进程和它的启动参数
- 如何实时监控 GC 状况并判断是否存在内存泄漏
- 如何分析堆内存中的对象分布
- 如何查看和修改 JVM 运行时参数
- 如何进行线程栈分析和死锁检测
- 如何用 jcmd 这把”瑞士军刀”统一操作
一、jps:快速定位 Java 进程
1.1 jps 是什么
如果你熟悉 Linux 的 ps 命令,那 jps 你一定能很快上手。jps(JVM Process Status Tool)的作用就是列出当前系统中所有正在运行的 Java 进程。
默认情况下,jps 输出两列信息:Java 进程 ID(PID)和主类名。通过追加参数,还能打印更多信息。
1.2 常用参数
| 参数 | 说明 |
|---|---|
-l | 打印主类的全限定名(包括包名),如果是 jar 包则打印 jar 路径 |
-v | 打印传递给 JVM 的参数(如 -Xmx、-XX:+UseG1GC 等) |
-m | 打印传递给主类 main 方法的参数 |
-q | 只输出进程 ID,不输出类名 |
1.3 使用示例
$ jps -mlv
18331 org.example.Foo Hello World
18332 jdk.jcmd/sun.tools.jps.Jps -mlv -Dapplication.home=/Library/Java/JavaVirtualMachines/jdk-11.jdk/Contents/Home -Xms8m -Djdk.module.main=jdk.jcmd
可以看到,输出中包含了进程 ID、主类名、主类参数、JVM 参数等详细信息,方便我们快速识别目标进程。
1.4 注意事项
有一点需要特别注意:如果 Java 进程关闭了 UsePerfData 参数(即使用 -XX:-UsePerfData),那么 jps(以及后面要介绍的 jstat)将无法探测到该进程。
拿到 Java 进程的 PID 之后,我们就可以使用后续的各种工具对其进行深入分析了。
二、jstat:性能数据实时监控
2.1 jstat 是什么
jstat(JVM Statistics Monitoring Tool)用于监控目标 Java 进程的各类性能数据,包括类加载、即时编译、垃圾回收等。它最大的特点是低开销,可以在生产环境中放心使用。
2.2 支持的子命令
jstat 通过不同的子命令来输出不同类型的数据:
$ jstat -options
-class
-compiler
-gc
-gccapacity
-gccause
-gcmetacapacity
-gcnew
-gcnewcapacity
-gcold
-gcoldcapacity
-gcutil
-printcompilation
这些子命令大致可以分为三类:
- 类加载相关:
-class - 即时编译相关:
-compiler、-printcompilation - 垃圾回收相关:所有以
-gc开头的子命令
2.3 基本用法
默认情况下,jstat 只打印一次数据。我们可以配置间隔时间和打印次数,实现周期性监控:
# 用法:jstat -outputOptions [-t] [-hlines] VMID [interval [count]]
$ jstat -gc 22126 1s 4
这条命令的含义是:监控 PID 为 22126 的进程,输出 GC 相关数据,每隔 1 秒打印一次,共打印 4 次。
监控本地 Java 进程时,VMID 就是 PID。如果需要监控远程进程,请参考 jstat 的官方文档。
2.4 GC 监控详解
-gc 是最常用的子命令,让我们看看它的输出:
S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT CGC CGCT GCT
17472.0 17472.0 0.0 0.0 139904.0 47146.4 349568.0 21321.0 30020.0 28001.8 4864.0 4673.4 22 0.080 3 0.270 0 0.000 0.350
各列含义如下:
| 列名 | 含义 |
|---|---|
| S0C / S1C | 两个 Survivor 区的容量(Capacity) |
| S0U / S1U | 两个 Survivor 区的已使用量(Utility) |
| EC / EU | Eden 区的容量和已使用量 |
| OC / OU | 老年代的容量和已使用量 |
| MC / MU | 元空间(Metaspace)的容量和已使用量 |
| CCSC / CCSU | 压缩类空间的容量和已使用量 |
| YGC / YGCT | 年轻代 GC 次数和总耗时 |
| FGC / FGCT | Full GC 次数和总耗时 |
| CGC / CGCT | 并发 GC Stop-The-World 的次数和总耗时 |
| GCT | 所有 GC 的总耗时 |
2.5 CMS vs G1 的输出差异
使用 CMS 垃圾回收器时,两个 Survivor 区的容量相等,且始终有一个 Survivor 区的使用量为 0。
但如果使用 G1 GC,输出结果会有明显不同:
S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT CGC CGCT GCT
0.0 16384.0 0.0 16384.0 210944.0 192512.0 133120.0 5332.5 28848.0 26886.4 4864.0 4620.5 19 0.067 1 0.016 2 0.002 0.084
你会发现:
S0C和S0U始终为 0S1C甚至可能下降到 0
这是因为 G1 GC 的内存布局与传统分代回收器不同。G1 将堆划分为多个等长的 Region,每个 Region 都可以充当 Eden、Survivor 或老年代,并且角色可以动态切换。
所以从逻辑上讲,G1 只有一个 Survivor 区。迁移 Survivor 数据时,只需申请新的 Region 作为新的 Survivor 即可。因此 JVM 把所有 Survivor Region 的总容量和使用量都放在 S1C/S1U 中,而 S0C/S0U 则设为 0。
当 Survivor 区的对象全部被回收或晋升时,这些 Region 会被回收,S1C 和 S1U 也会变成 0。
2.6 实用技巧
1. 查看进程启动时间
jstat 的 -t 参数会在每行数据前打印目标 Java 进程的启动时间(单位:秒):
$ jstat -gc -t 22407
Timestamp S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT CGC CGCT GCT
10.7 0.0 0.0 0.0 0.0 55296.0 45056.0 34816.0 20267.8 30128.0 27975.3 4864.0 4671.6 33 0.086 3 0.111 2 0.001 0.198
通过比较进程运行时间和 GC 总时间(GCT 列),可以计算出 GC 时间占运行时间的比例:
- 如果超过 20%,说明堆压力较大
- 如果超过 90%,说明堆几乎满了,随时可能 OOM
2. 判断内存泄漏
jstat 还可以用来初步判断是否存在内存泄漏。方法是:
- 长时间运行
jstat,连续获取多行数据 - 取每组数据中 OU(老年代已使用内存)的最小值
- 每隔一段时间重复一次,得到多组 OU 最小值
- 如果这些最小值呈持续上涨趋势,说明老年代中无法回收的对象在不断增加,很可能存在内存泄漏
三、jmap:堆内存深度分析
3.1 jmap 是什么
当怀疑有内存泄漏或 OOM 问题时,jmap(JVM Memory Map)是最常用的分析工具。它可以统计堆中的对象分布,甚至导出整个堆的快照供后续分析。
3.2 常用子命令
| 子命令 | 说明 |
|---|---|
-clstats | 打印已加载类的统计信息 |
-finalizerinfo | 打印所有待 finalize 的对象 |
-histo | 统计每个类的实例数量和占用内存,按内存使用量排序 |
-histo:live | 只统计存活对象(会触发 Full GC) |
-dump | 导出堆快照 |
-dump:live | 只导出存活对象的堆快照(会触发 Full GC) |
3.3 直方图分析:-histo
-histo 子命令用来统计堆中各类对象的实例数量和内存占用:
$ jmap -histo 22574
num #instances #bytes class name (module)
-------------------------------------------------------
1: 500004 20000160 org.python.core.PyComplex
2: 570866 18267712 org.python.core.PyFloat
3: 360295 18027024 [B (java.base@11)
4: 339394 11429680 [Lorg.python.core.PyObject;
5: 308637 11194264 [Ljava.lang.Object; (java.base@11)
6: 301378 9291664 [I (java.base@11)
7: 225103 9004120 java.math.BigInteger (java.base@11)
8: 507362 8117792 org.python.core.PySequence$1
9: 285009 6840216 org.python.core.PyLong
10: 282908 6789792 java.lang.String (java.base@11)
...
Total 5151277 167944400
通过这个输出,我们可以快速定位哪些类的对象占用了最多的内存,这在分析内存泄漏时非常有用。
3.4 导出堆快照:-dump
最常用的还是导出堆快照,命令格式如下:
$ jmap -dump:live,format=b,file=heap.bin <pid>
参数说明:
live:只导出存活对象(可选,不加则导出所有对象)format=b:导出为二进制格式(hprof 格式)file=heap.bin:导出到指定文件
导出的二进制文件可以用 MAT(Memory Analyzer Tool)、VisualVM 等 GUI 工具进行深入分析,我们在下一篇介绍 GUI 工具时会详细演示。
3.5 注意事项
jmap 在执行时需要借助安全点机制,让所有应用线程都停在不改变堆数据的位置。这意味着:
- 堆快照必定是安全点位置的,这可能导致分析结果存在偏差。比如某些对象的生命周期在两个安全点之间,
:live选项可能无法探知到这些对象。 - 如果某个线程长时间无法到达安全点,
jmap会一直等下去。比如下面这段代码,由于是计数循环且Math.log是 intrinsic 方法,长时间不会进入安全点:
static double sum = 0;
public static void main(String[] args) {
for (int i = 0; i < 0x77777777; i++) { // 计数循环
sum += Math.log(i); // Math.log 是 intrinsic 方法
}
}
相比之下,jstat 就没有这个问题——因为垃圾回收器会主动把 jstat 需要的统计数据保存到固定位置,jstat 只需直接读取即可。
jmap(以及后面的jinfo、jstack、jcmd)都基于 Attach API,只能监控本地 Java 进程。如果开启了-XX:+DisableAttachMechanism参数,这些工具都将无法使用。
四、jinfo:查看和修改 JVM 参数
4.1 jinfo 是什么
jinfo(JVM Configuration Info)用来查看目标 Java 进程的配置信息,包括 JVM 参数、系统属性等。它还支持动态修改部分 JVM 参数。
4.2 查看参数
直接运行 jinfo <pid> 会输出三类信息:
- Java System Properties:通过
System.getProperty()获取的系统属性,也就是-D参数 - VM Flags:所有的
-XX参数 - VM Arguments:JVM 启动参数(
-X参数等)和主类信息
$ jinfo 31185
Java System Properties:
gopherProxySet=false
awt.toolkit=sun.lwawt.macosx.LWCToolkit
java.specification.version=11
...
VM Flags:
-XX:CICompilerCount=4 -XX:ConcGCThreads=3 -XX:G1ConcRefinementThreads=10 -XX:G1HeapRegionSize=2097152 ...
VM Arguments:
jvm_args: -Xlog:gc -Xmx1024m
java_command: org.example.Foo
...
4.3 动态修改参数
jinfo 最实用的功能之一是可以动态修改标记为 manageable 的 JVM 参数,而无需重启进程。
例如,开启 HeapDumpAfterFullGC 参数:
$ jinfo -flag +HeapDumpAfterFullGC <pid>
再比如,修改最大堆空闲比例:
$ jinfo -flag MaxHeapFreeRatio=80 <pid>
那么哪些参数支持动态修改呢?可以用下面这条命令查看所有 manageable 参数:
$ java -XX:+PrintFlagsFinal -version | grep manageable
intx CMSAbortablePrecleanWaitMillis = 100 {manageable} {default}
intx CMSTriggerInterval = -1 {manageable} {default}
intx CMSWaitDuration = 2000 {manageable} {default}
bool HeapDumpAfterFullGC = false {manageable} {default}
bool HeapDumpBeforeFullGC = false {manageable} {default}
bool HeapDumpOnOutOfMemoryError = false {manageable} {default}
ccstr HeapDumpPath = {manageable} {default}
uintx MaxHeapFreeRatio = 70 {manageable} {default}
uintx MinHeapFreeRatio = 40 {manageable} {default}
bool PrintClassHistogram = false {manageable} {default}
bool PrintConcurrentLocks = false {manageable} {default}
在排查线上问题时,这个功能非常有用——比如可以临时打开 GC 日志、开启 OOM 时自动 dump 堆等。
五、jstack:线程栈与死锁分析
5.1 jstack 是什么
jstack 用来打印目标 Java 进程中各个线程的栈轨迹,以及这些线程持有的锁信息。它是排查线程死锁、CPU 飙高、线程阻塞等问题的利器。
5.2 死锁检测示例
jstack 最经典的应用场景就是死锁检测。我们来看一个已经发生死锁的 Java 程序的输出:
$ jstack 31634
...
"Thread-0" #12 prio=5 os_prio=31 cpu=1.32ms elapsed=34.24s tid=0x00007fb08601c800 nid=0x5d03 waiting for monitor entry [0x000070000bc7e000]
java.lang.Thread.State: BLOCKED (on object monitor)
at DeadLock.foo(DeadLock.java:18)
- waiting to lock <0x000000061ff904c0> (a java.lang.Object)
- locked <0x000000061ff904b0> (a java.lang.Object)
at DeadLock$$Lambda$1/0x0000000800060840.run(Unknown Source)
at java.lang.Thread.run(java.base@11/Thread.java:834)
"Thread-1" #13 prio=5 os_prio=31 cpu=1.43ms elapsed=34.24s tid=0x00007fb08601f800 nid=0x5f03 waiting for monitor entry [0x000070000bd81000]
java.lang.Thread.State: BLOCKED (on object monitor)
at DeadLock.bar(DeadLock.java:33)
- waiting to lock <0x000000061ff904b0> (a java.lang.Object)
- locked <0x000000061ff904c0> (a java.lang.Object)
at DeadLock$$Lambda$2/0x0000000800063040.run(Unknown Source)
at java.lang.Thread.run(java.base@11/Thread.java:834)
...
Found one Java-level deadlock:
=============================
"Thread-0":
waiting to lock monitor 0x00007fb083015900 (object 0x000000061ff904c0, a java.lang.Object),
which is held by "Thread-1"
"Thread-1":
waiting to lock monitor 0x00007fb083015800 (object 0x000000061ff904b0, a java.lang.Object),
which is held by "Thread-0"
...
Found 1 deadlock.
可以看到,jstack 的输出非常详细:
- 每个线程的名称、优先级、线程 ID、状态
- 完整的调用栈轨迹
- 每个线程持有的锁(
locked ...) - 每个线程正在等待的锁(
waiting to lock ...) - 自动检测并报告死锁
在输出的末尾,jstack 会自动分析并明确告诉我们存在几处死锁,以及每个死锁涉及哪些线程、哪些锁。
5.3 常见线程状态
通过 jstack 输出,我们可以看到各种线程状态:
| 状态 | 说明 |
|---|---|
RUNNABLE | 线程正在运行或就绪 |
BLOCKED | 线程阻塞,等待获取 monitor 锁 |
WAITING | 无限期等待,等待其他线程唤醒 |
TIMED_WAITING | 有时限的等待,如 sleep()、wait(timeout) |
TERMINATED | 线程已终止 |
六、jcmd:瑞士军刀
6.1 jcmd 是什么
jcmd 是 JDK 7 之后新增的多功能诊断工具,可以说是一把”瑞士军刀”。它整合了前面除了 jstat 之外几乎所有命令的功能。
换句话说,jps、jmap、jinfo、jstack 能做的事情,jcmd 几乎都能做。
6.2 功能对照
大致的功能对应关系如下:
| 原命令 | jcmd 替代 |
|---|---|
jps -l | jcmd 或 jcmd -l |
jinfo <pid> | jcmd <pid> VM.system_properties + jcmd <pid> VM.flags |
jmap -histo <pid> | jcmd <pid> GC.class_histogram |
jmap -dump <pid> | jcmd <pid> GC.heap_dump filename.bin |
jstack <pid> | jcmd <pid> Thread.print |
至于 jstat 的功能,虽然 jcmd 也复制了部分代码,支持通过 PerfCounter.print 子命令打印所有性能计数器,但它没有保留 jstat 那样友好的输出格式,也不支持周期性打印。所以在监控 GC 方面,jstat 仍然是首选。
6.3 支持的子命令
jcmd 支持的子命令非常丰富,下面列出一些常用的:
编译器相关:
Compiler.CodeHeap_Analytics:代码堆分析Compiler.codecache:代码缓存统计Compiler.codelist:已编译方法列表Compiler.queue:编译队列
GC 相关:
GC.class_histogram:类直方图GC.class_stats:类统计信息GC.finalizer_info:Finalizer 信息GC.heap_dump:导出堆快照GC.heap_info:堆信息概览GC.run:手动触发 GCGC.run_finalization:手动触发 finalization
JVM 相关:
VM.class_hierarchy:类层次结构VM.classloader_stats:类加载器统计VM.command_line:命令行参数VM.flags:JVM 参数VM.info:JVM 综合信息VM.metaspace:元空间信息VM.native_memory:Native Memory TrackingVM.set_flag:设置 manageable 参数VM.system_properties:系统属性VM.uptime:进程运行时间VM.version:JVM 版本
此外,jcmd 还支持 Java Flight Recorder(JFR)相关的功能,我们将在下一篇 GUI 工具中一并介绍。
总结与思考
本文要点回顾
- jps:快速定位 Java 进程,支持输出主类、JVM 参数、主类参数等信息。
- jstat:低开销的性能监控工具,常用于监控 GC 状况、判断内存泄漏。支持周期性输出,是生产环境的首选。
- jmap:堆内存分析工具,可以统计对象分布(
-histo)和导出堆快照(-dump)。需要注意安全点问题带来的影响。 - jinfo:查看和动态修改 JVM 参数。支持修改标记为 manageable 的参数,线上排查问题时非常实用。
- jstack:线程栈分析工具,可以打印所有线程的栈轨迹和锁信息,还能自动检测死锁。
- jcmd:多功能诊断工具,整合了前面大部分工具的功能,是 JDK 7+ 推荐使用的统一入口。
思考与实践
- 回想一下你遇到过的线上性能问题,如果用今天学到的工具,你会怎么排查?
- 动手试试
jcmd的各项功能,看看哪些监控项对你的项目有帮助。 - 思考一下,这些命令行工具各自的优缺点是什么?分别适合什么场景?
在下一篇文章中,我们将继续介绍 JVM 监控诊断的 GUI 工具,包括 VisualVM、JMC、JFR 等,让你更直观地分析 JVM 的运行状态。








请登录后查看评论内容