
1. 为什么命令行工具依然是JVM排查的首选干了这么多年后端我越来越觉得图形化监控工具虽然直观但真正到了线上出问题的时候能救命的往往还是那几个命令行工具。原因很简单生产环境通常不允许你随便装软件、开端口甚至很多时候连外网都连不上但JDK自带的那些命令只要有Java进程在跑就能直接用。这个项目标题“深入理解JVM - JVM常用命令”说白了就是要把JDK自带的那套诊断工具链吃透。它解决的核心问题是当Java应用出现CPU飙高、内存泄漏、线程死锁、GC频繁等疑难杂症时你如何在最短时间内定位到根因。适合谁看我觉得只要你是Java后端开发或者正在往资深工程师方向走这些命令就是基本功跟会用Git一样属于必须刻在肌肉记忆里的东西。我见过太多人一遇到问题就重启或者只会加日志重新发版效率极低。实际上JVM自带的工具能让你在不重启、不改代码的前提下直接看到堆内存里有什么对象、线程卡在哪里、GC到底花了多少时间。这篇文章我会按照实际排查场景来组织把每个命令的用法、参数、输出解读、以及我踩过的坑都讲清楚让你看完就能直接上手。2. JVM命令工具链的整体设计与选型逻辑2.1 为什么JDK要自带这些工具JDK自带的诊断工具设计哲学其实很明确轻量、无侵入、随取随用。它们大部分是通过Attach API机制连接到目标JVM进程的也就是说只要目标进程开启了Attach机制默认开启你就能从外部“挂”上去读取运行时的各种数据。这些工具大致可以分成几类一类是进程状态查看比如jps一类是运行时信息导出比如jinfo、jmap、jstack还有一类是性能监控比如jstat、jcmd。它们各自负责不同的数据维度组合起来就能覆盖绝大部分排查场景。注意从JDK 9开始部分工具被整合进了jcmd官方推荐优先使用jcmd但老工具依然保留很多生产环境还在用JDK 8所以两者都得会。2.2 工具选型的核心考量我选工具的逻辑很简单先看现象再选工具。CPU高就先看线程内存涨就先看堆GC频繁就先看统计。不要一上来就把所有命令跑一遍那样既浪费时间也可能因为频繁Attach对目标进程造成额外压力。举个例子如果用户反馈“接口变慢”我会先用jstat -gcutil看GC情况如果发现Full GC频繁再用jmap导出堆快照分析如果GC正常那就用jstack看线程状态排查锁竞争或者阻塞。这个顺序很重要能让你少走弯路。2.3 各命令的定位与适用边界命令核心用途适用场景是否影响目标进程jps查看Java进程列表快速定位PID几乎无影响jstat查看GC统计信息GC频繁、内存增长极低jmap导出堆快照、查看堆信息内存泄漏、OOM导出快照时会STWjstack导出线程栈死锁、CPU高、阻塞极低jinfo查看/修改JVM参数确认参数配置极低jcmd综合命令替代多个工具视子命令而定这张表我建议你记在心里排查时先对照现象选工具效率会高很多。3. 核心命令逐一拆解与实操要点3.1 jps一切排查的起点jps的作用很简单就是列出当前用户有权限访问的所有Java进程。它的输出格式是PID 主类名加上-l参数可以显示完整包名-v可以显示JVM启动参数。jps -lvm这条命令我几乎每次排查都会先跑一遍因为你需要拿到PID才能做后续操作。-v参数特别有用能直接看到启动时的堆大小、GC参数等省得再去翻启动脚本。实操心得如果jps输出为空但明明有Java进程在跑通常是权限问题。目标进程属于其他用户而你当前用户没有Attach权限。这时候要么切到对应用户要么用sudo但要注意sudo可能改变环境变量导致找不到命令。3.2 jstatGC问题的第一双眼睛jstat是我用得最多的工具没有之一。它能以极低的开销持续输出GC相关的统计数据特别适合观察GC频率和耗时。最常用的命令是jstat -gcutil pid 1000 10意思是每隔1秒输出一次共输出10次。输出字段包括各内存区域的使用率S0、S1、E、O、M、YGC次数、YGCT耗时、FGC次数、FGCT耗时、GCT总耗时。我一般会重点关注三个指标FGC是否持续增长、FGCT是否过大、老年代使用率是否居高不下。如果FGC每分钟超过几次或者单次FGCT超过1秒那就说明GC有问题了需要进一步用jmap分析。jstat -gc pid 1000 5-gc参数输出的是具体的内存大小KB而不是百分比适合计算对象增长速率。比如你观察Eden区每次GC后回收了多少就能估算出对象分配速率。注意jstat的输出是瞬时值不要只看一次就下结论。至少观察1-2分钟看趋势才有意义。3.3 jmap堆内存的显微镜jmap有两个核心用途查看堆信息、导出堆快照。查看堆信息jmap -heap pid这条命令会输出堆的配置和使用情况包括各区域的容量、已用空间、GC算法等。不过说实话这个命令在JDK 8之后对G1等新GC支持不够好输出可能不完整我更推荐用jcmd的GC.heap_info。导出堆快照jmap -dump:live,formatb,fileheap.hprof pidlive表示只导出存活对象这样文件会小很多但会触发一次Full GC。如果不加live导出的是所有对象包括垃圾文件大但不会强制GC。重要提醒导出堆快照会暂停应用STW时间长短取决于堆大小可能几秒到几十秒。生产环境操作前一定要评估影响最好在低峰期做并且确保磁盘空间足够堆快照文件通常和堆大小相当甚至更大。3.4 jstack线程问题的照妖镜jstack用来导出线程栈是排查死锁、CPU飙高、线程阻塞的利器。jstack pid thread.txt输出内容包含每个线程的状态、调用栈、锁信息。我一般会重点看几种状态BLOCKED表示等待锁WAITING表示无限等待TIMED_WAITING表示限时等待。如果大量线程处于BLOCKED那大概率有锁竞争。排查CPU高的时候我会先用top -Hp pid找到CPU最高的线程ID然后转换成十六进制再在jstack输出里搜索这个十六进制ID就能定位到具体是哪段代码在消耗CPU。printf %x\n tid这个转换很关键因为jstack输出里的nid是十六进制的。实操心得jstack连续执行三次间隔几秒然后对比线程栈的变化。如果某个线程一直停在同一个地方那它就是“嫌疑线程”。这个方法比只看一次准确得多。3.5 jinfo参数确认的快捷方式jinfo主要用来查看JVM的运行时参数包括默认值和实际生效值。jinfo -flags pid jinfo -sysprops pid-flags看JVM参数-sysprops看系统属性。有时候你怀疑某个参数没生效用这个命令一查便知。它还支持动态修改部分参数比如-flag PrintGC但能改的参数很有限大部分需要重启。3.6 jcmd新时代的瑞士军刀jcmd是JDK 7引入的综合工具到了JDK 9之后基本可以替代前面大部分命令。它的用法是jcmd pid command。常用子命令jcmd pid help jcmd pid VM.flags jcmd pid GC.heap_info jcmd pid Thread.print jcmd pid GC.class_histogramGC.class_histogram特别有用能直接输出堆中各类对象的数量和大小不用导出快照就能快速定位内存大户。我经常用它来替代jmap -histo因为jcmd更稳定。注意jcmd的子命令在不同JDK版本可能有差异用之前先help一下确认当前版本支持哪些。4. 完整排查流程与真实场景复现4.1 场景一CPU飙高的排查过程假设某天收到告警某Java服务CPU持续90%以上。我的操作顺序是这样的第一步jps -l找到目标PID。第二步top -Hp pid找到CPU最高的几个线程。第三步把线程ID转成十六进制。第四步jstack pid导出线程栈搜索十六进制ID。第五步定位到具体代码后结合业务逻辑分析原因。这个过程我做过无数次最常见的原因是死循环、频繁Full GC、正则回溯。如果是GC导致的CPU高jstack里会看到大量GC线程在跑这时候就要转去用jstat看GC情况了。4.2 场景二内存泄漏的定位方法内存泄漏的典型表现是老年代使用率持续上升Full GC后也降不下来。我的排查步骤先用jstat -gcutil pid 1000观察几分钟确认老年代确实在涨。然后用jcmd pid GC.class_histogram看对象分布找出数量异常多的类。如果直方图不够直观就导出堆快照用分析工具打开看支配树和引用链。实操心得导出堆快照前先执行一次Full GCjcmd pid GC.run这样快照里主要是存活对象分析起来更清晰。但要注意Full GC本身有开销低峰期操作。4.3 场景三线程死锁的快速确认死锁的排查最简单jstack会自动检测并输出死锁信息。你只需要jstack pid | grep -A 20 deadlock如果存在死锁输出里会明确告诉你哪两个线程互相持有对方需要的锁以及具体的代码行号。这时候直接去看代码调整加锁顺序就能解决。4.4 参数计算与选择过程举个例子假设你通过jstat发现Full GC后老年代使用率仍有80%且每次Full GC只能回收很少内存这说明有大量对象长期存活。这时候你需要估算堆大小是否合理。假设应用每秒产生100MB对象老年代存活对象约2GBFull GC耗时2秒。如果堆总共4GB老年代2GB那基本没有余量稍微波动就会频繁Full GC。合理的做法是增大堆比如调到8GB给老年代留足空间。但也不能无限大因为Full GC时间会随堆增大而增加一般建议单次Full GC控制在1秒以内。这个计算没有绝对公式需要结合GC日志和实际观察来调。我一般会先用-Xmx和-Xms设成一样避免堆动态调整带来的抖动然后根据GC频率逐步调整。5. 常见问题与排查技巧实录5.1 命令执行报错怎么办报错信息原因解决方法Unable to open socket fileAttach机制被禁用检查-XX:DisableAttachMechanismPermission denied用户权限不足切换到目标进程用户或提权well-known file is not secure临时目录权限问题检查/tmp权限或指定-Djava.io.tmpdirpid not foundPID错误或进程已退出用jps重新确认5.2 生产环境使用的注意事项第一不要在高峰期导出堆快照STW可能导致请求超时。第二jstack连续执行不要超过5次频繁Attach有开销。第三jstat观察间隔不要太短1秒足够太短反而影响目标进程。第四所有操作前先确认磁盘空间堆快照可能几十GB。5.3 我踩过的坑有一次我在容器里执行jmap一直报错找不到进程。后来发现是容器内PID和宿主机PID不一致jps显示的是容器内PID但Attach时用的临时文件路径有问题。解决办法是确保/tmp可写或者用jcmd替代它对容器环境兼容性更好。还有一次我用jstack排查CPU高结果发现线程栈里全是GC task thread说明是GC导致的。这时候再去看jstat果然Full GC频繁。所以工具之间要联动不要孤立地看一个命令的输出。5.4 常用命令速查表需求命令查看Java进程jps -lvm查看GC统计jstat -gcutil pid 1000查看堆信息jcmd pid GC.heap_info导出堆快照jmap -dump:live,formatb,fileheap.hprof pid导出线程栈jstack pid thread.txt查看对象直方图jcmd pid GC.class_histogram查看JVM参数jinfo -flags pid手动触发GCjcmd pid GC.run这套命令组合我用了很多年基本上覆盖了日常排查的90%场景。剩下的10%可能需要更专业的工具但那些通常不是一线开发需要操心的。最后分享一个小技巧把常用的命令写成别名或者脚本比如alias jgcjstat -gcutil能省不少时间。另外jcmd的help一定要常看不同版本差异挺大的别凭记忆硬套。