这个系列
「JVM 线上排查实战」第七篇。前六篇教的所有命令,都有一个前提:工具能连上那个进程。
- (一)先把 JVM 看清楚:进程、参数、默认值
- (二)CPU 飙高:找到那个线程
- (三)线程卡住:死锁、BLOCKED、线程池打满
- (四)内存:OOM 了先干什么
- (五)GC 日志:从 JDK 8 升 17,老启动参数会让进程直接起不来
- (六)JFR 飞行记录器:录一段现场下来慢慢看
- (七)工具连不上进程时怎么办 —— 本篇
这一篇把「连不上」的几种情况逐个构造出来,贴每一种的原始报错,并给出还能用什么。
实测环境:CentOS 7.9.2009(内核 3.10.0-1160.71.1.el7.x86_64),同机三个 JDK —— Oracle 1.8.0_381(/opt/jdk8)、OpenJDK 1.8.0_412(/usr/lib/jvm/java-1.8.0-openjdk)、Oracle 17.0.8(/usr/java)。测试程序是一个死循环烧 CPU + 不停分配对象的小程序。
先说结论
jps看不到,不代表进程有问题,更不代表你连不上它。jps靠的是/tmp/hsperfdata_<用户>/<pid>这个文件,而 attach 靠的是另一套东西 —— 三种情况下jps空着但jcmd <pid>照样能用- 换个用户就看不见了:
appuser跑jps只看得到自己那一个,root 的三个进程一个都不显示 - 普通用户 attach root 的进程,报的是「不允许的操作」,不是「找不到进程」
- 进程被
kill -STOP之后,jstack和jcmd在限时内都不返回(60 秒 / 30 秒,退出码 124),kill -CONT之后立刻恢复正常 -XX:+DisableAttachMechanism是最坑的一种:jps里还看得见这个进程,但所有 attach 工具全废- 跨版本 attach 要看工具走哪条路:
jstack/jcmd(走 attach)两个方向都能用;jmap -heap(走 SA)跨版本直接抛异常 jstack -F在 JDK 17 上已经没了,报错会告诉你改用jhsdb jstack- 最后一条后路是
kill -3:线程栈打到进程自己的标准输出里,连 attach 被禁用时它都还能用
1. jps 看不到进程:三种原因 ✅
1.1 你和进程不是同一个用户
root 下看,四个 Java 进程都在:
$ /usr/java/bin/jps -l
5842 JfrDemo
6706 jdk.jcmd/sun.tools.jps.Jps
6679 JfrDemo
5737 JfrDemo
5739 JfrDemo
切到 appuser(它自己跑了其中一个,pid 6679),同一条命令:
$ su - appuser -c '/usr/java/bin/jps -l'
6679 JfrDemo
6727 jdk.jcmd/sun.tools.jps.Jps
root 的三个进程连影子都没有。 不是权限报错,是干脆不显示 —— 这也是「jps 看不到进程」最常见的原因:你用普通账号登录,而服务是另一个账号起的。
反过来 root 能看到所有用户的(上面那个 6679 就是 appuser 的)。
1.2 /tmp 下的 hsperfdata 文件被清掉了
jps 的数据来源是这个目录:
$ ls /tmp/hsperfdata_root/
5737 5739 5842
一个文件对一个 pid。很多机器上有定时清理 /tmp 的任务,把它删掉之后:
$ rm -f /tmp/hsperfdata_root/5739
$ /usr/java/bin/jps -l
5842 JfrDemo 6679 JfrDemo 5737 JfrDemo 6829 jdk.jcmd/sun.tools.jps.Jps
5739 没了。但进程活得好好的,按 pid 直接用 jcmd 一点问题都没有:
$ /usr/java/bin/jcmd 5739 VM.uptime
5739:
323.039 s
🔑 jps 空 ≠ 连不上。 这一条能省掉很多无谓的折腾:从 ps -ef | grep java 里拿到 pid,照样往下查。
1.3 启动参数里关掉了 UsePerfData
$ /usr/java/bin/java -XX:-UsePerfData -cp c17 JfrDemo
这个进程(pid 6894)在 jps 里同样是不存在的:
$ /usr/java/bin/jps -l | grep -c "^6894 "
0
而 jstack 照样连:
$ /usr/java/bin/jstack 6894
2026-09-23 22:47:46
Full thread dump Java HotSpot(TM) 64-Bit Server VM (17.0.8+9-LTS-211 mixed mode, sharing):
2. jstack 报「不允许的操作」✅
用 appuser 去 attach root 的进程:
$ su - appuser -c '/usr/java/bin/jstack 5739'
5739: 不允许的操作
(英文环境下是 Operation not permitted。)
反过来 root 去 attach 普通用户的进程,可以:
$ /usr/java/bin/jstack 6679
2026-09-23 22:47:16
Full thread dump Java HotSpot(TM) 64-Bit Server VM (17.0.8+9-LTS-211 mixed mode, sharing):
→ attach 要么同用户,要么用 root。 看到「不允许的操作」先看 ps -ef | grep <pid> 第一列是谁。
3. 进程被暂停了:命令挂在那儿不返回 ✅
kill -STOP 把进程挂起(线上等价的情况是进程卡在不可中断状态、或者被调试器停住):
$ kill -STOP 5842
$ timeout 60 /opt/jdk8/bin/jstack 5842 > stop8.txt 2>&1; echo "jstack 退出码=$? 输出 $(stat -c%s stop8.txt) 字节"
jstack 退出码=124 输出 0 字节
124 是 timeout 杀掉它的退出码 —— 60 秒内 jstack 没有返回任何东西。换 jcmd 也一样:
$ timeout 30 /opt/jdk8/bin/jcmd 5842 Thread.print > stopc.txt 2>&1; echo "jcmd 退出码=$? 输出 $(stat -c%s stopc.txt) 字节"
jcmd 退出码=124 输出 6 字节
那 6 个字节只是它先打的一行 5842:,后面什么都没有 —— 看起来像是在输出,其实已经卡住了。
恢复之后立刻就正常:
$ kill -CONT 5842
$ timeout 30 /opt/jdk8/bin/jstack 5842 > cont8.txt 2>&1; echo "退出码=$? 输出 $(stat -c%s cont8.txt) 字节"
退出码=0 输出 3836 字节
🔑 attach 是要目标进程自己配合的(它要起一个 Attach Listener 线程来响应)。进程被停住 = 没人接你的电话。所以 jstack 敲下去半天没反应时,先看 ps -o stat= -p <pid>:T 就是被停住了。
4. 最坑的一种:jps 看得见,但谁都连不上 ✅
启动参数里带了 -XX:+DisableAttachMechanism(有些安全加固基线会要求加它):
$ /usr/java/bin/java -XX:+DisableAttachMechanism -cp c17 JfrDemo
jstack:
$ /usr/java/bin/jstack 7138
7138: The VM does not support the attach mechanism
[退出码=1]
jcmd:
$ /usr/java/bin/jcmd 7138 Thread.print
7138:
com.sun.tools.attach.AttachNotSupportedException: The VM does not support the attach mechanism
at jdk.attach/sun.tools.attach.HotSpotAttachProvider.testAttachable(HotSpotAttachProvider.java:154)
[退出码=1]
而 jps 里它还好端端地列着:
$ /usr/java/bin/jps -l | grep -c "^7138 "
1
🔑 这就是为什么不能拿 jps 判断"工具能不能用":前面 1.2 / 1.3 是「jps 看不见但连得上」,这里正好反过来,是「jps 看得见但连不上」。这两件事测的根本不是同一样东西。
这种情况下,下面第 6 节那条后路仍然有效。
5. 跨版本 attach:能不能用,取决于工具走哪条路 ✅
同一台机器上有 JDK 8 和 17 的进程,手边的工具却只有一个版本时:
| 用谁的工具 | 打谁的进程 | 结果 |
|---|---|---|
JDK 17 jstack | JDK 8 进程 | ✅ 正常输出(Full thread dump … 25.381-b09) |
JDK 8 jstack | JDK 17 进程 | ✅ 正常输出(Full thread dump … 17.0.8+9-LTS-211) |
JDK 17 jcmd | JDK 8 进程 | ✅ 正常输出 |
JDK 8 jmap -heap | JDK 17 进程 | ❌ 抛异常 |
jmap -heap 那次的原文:
$ /opt/jdk8/bin/jmap -heap 5739
Attaching to process ID 5739, please wait...
Exception in thread "main" java.lang.reflect.InvocationTargetException
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
🔑 分界在工具走哪条路:jstack/jcmd 走的是 attach 协议(让目标进程自己打印),对版本宽容;jmap -heap 走的是 Serviceability Agent,要按目标 JVM 的内存结构去解析,版本对不上就炸。
这也解释了(四)里那个现象:
jmap -heap在 17 上要换成jhsdb jmap --heap --pid。
jstack -F 在 JDK 17 上没了
JDK 8 上 -F 还在(它走的也是 SA):
$ /opt/jdk8/bin/jstack -F 5737
Attaching to process ID 5737, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.381-b09
Deadlock Detection:
JDK 17:
$ /usr/java/bin/jstack -F 5739
Error: -F option used
Cannot connect to core dump or remote debug server. Use jhsdb jstack instead
→ 17 上把 jstack -F 换成 jhsdb jstack --pid <pid>。
6. 最后那条后路:kill -3 ✅
所有 attach 工具都用不了的时候,还有一个办法:给进程发 SIGQUIT,JVM 会把线程栈打到自己的标准输出里。
$ ls -la d17.log # 发信号前
17 字节
$ kill -3 5739
$ ls -la d17.log # 发信号后
7401 字节
$ grep -c 'Full thread dump' d17.log
1
内容和 jstack 是一样的东西:
Full thread dump Java HotSpot(TM) 64-Bit Server VM (17.0.8+9-LTS-211 mixed mode, sharing):
Threads class SMR info:
_java_thread_list=0x00007fc8240028c0, length=16, elements={
连第 4 节那个 attach 被禁用的进程,kill -3 照样拿得到:
$ kill -3 7138
da.log 17 -> 5534 字节
$ grep -c 'Full thread dump' da.log
1
⚠️ 两个前提:
- 它打到的是进程的标准输出,也就是你启动脚本里重定向的那个文件(
nohup.out、catalina.out、xxx.log)。如果启动时把 stdout 丢进了/dev/null,那这条路也断了 —— 这是启动脚本该现在就去改的一件事 kill -3不会杀死进程(SIGQUIT被 JVM 接管了),但别对kill -9抱同样的期待,那个是真杀
7. 顺带说清 attach 到底靠什么文件 ✅
attach 用的是 /tmp 下的一个 socket 文件,名字是 .java_pid<pid>:
$ ls /tmp/.java_pid*
/tmp/.java_pid5737 /tmp/.java_pid5739 /tmp/.java_pid5842 /tmp/.java_pid6679 …
它不看 java.io.tmpdir。 启动时指定 -Djava.io.tmpdir=/root/tmpx 的进程(pid 7253),attach 一样成功:
$ /usr/java/bin/jstack 7253 > td_jstack.txt 2>&1; echo "退出码=$? 输出 $(stat -c%s td_jstack.txt) 字节"
退出码=0 输出 5446 字节
而 socket 文件仍然落在 /tmp,指定的那个目录是空的:
$ ls -a /root/tmpx
. ..
顺带一个细节:/tmp 下能看到早就退出的进程留下的 .java_pid 文件(实测里有 .java_pid77783 这种,进程早没了)。所以别拿 /tmp/.java_pid<pid> 在不在来判断进程是否健在,那只是个残留文件。
8. 本篇速查
| 现象 | 先查什么 | 还能用什么 |
|---|---|---|
jps 什么都不显示 | 你和进程是不是同一个用户(ps -ef | grep java 第一列) | ps 拿 pid → jcmd <pid> 照样能用 |
jps 少了某个进程 | /tmp/hsperfdata_<用户>/ 下有没有那个 pid 文件;启动参数有没有 -XX:-UsePerfData | 同上,按 pid 直接连 |
<pid>: 不允许的操作 | 进程属主是谁 | 切成同一个用户,或用 root |
| 命令敲下去不返回 | ps -o stat= -p <pid> 是不是 T(被 STOP) | kill -CONT 恢复后再查;或 kill -3 看日志 |
The VM does not support the attach mechanism | 启动参数有没有 -XX:+DisableAttachMechanism | kill -3,从进程日志里读栈 |
| 手边 JDK 版本和进程对不上 | 工具走 attach 还是 SA | jstack/jcmd 可以跨版本;jmap -heap 不行 |
jstack -F 在 17 上报错 | — | jhsdb jstack --pid <pid> |
| 什么工具都连不上 | 启动脚本有没有把 stdout 丢掉 | kill -3 + 看进程自己的日志 |