最直接有效的方式是用jstat和jstack现场定位死锁:先用jstat -gc -t <pid> 1000 3排除GC异常,再用jstack -l <pid> | grep -A 10 -B 10 "waiting to lock|BLOCKED"抓取锁信息,比对多次快照确认线程状态是否持续阻塞,最后通过栈帧回溯定位业务代码中的加锁顺序问题。

遇到 Java 进程卡死、响应停滞,又没开 JMX、没挂远程调试端口,最直接有效的方式就是用 jstat 和 jstack 在现场快速定位死锁线程——不需要重启、不依赖外部监控,只要进程还在运行、你有 shell 权限,就能上手。
先用 jstat 看 JVM 是否真卡在锁竞争上
jstat 不查线程状态,但能帮你排除干扰:确认不是 GC 撑爆、内存耗尽或元空间打满导致的假性“卡死”。死锁本身不触发频繁 GC,但如果伴随大量对象创建或锁持有时间过长,可能间接引发 GC 压力。
- 执行
jstat -gc <pid>:观察YGCT/FGCT是否异常飙升,EU/OU是否持续高位不回收 —— 若 GC 正常,才继续怀疑死锁 - 加
-t参数看时间戳:jstat -gc -t <pid> 1000 3(每秒采样 3 次),对比 GC 时间与业务停顿是否同步 - 顺手跑
jstat -compiler <pid>看 JIT 编译是否卡住(failed或invalid多可能是类加载/反射问题,非死锁)
用 jstack 直接抓取线程快照并过滤死锁线索
jstack <pid> 是核心动作。重点不是“全量输出”,而是快速聚焦可疑线程块:
将一个或多个视频上传至YouTube。当用户想“上传到YouTube”“发YouTube”“批量上传”“upload to YouTube”“post videos to YouTube”或发布到YouTube时使用此技能。
- 加
-l参数:jstack -l <pid>—— 输出带锁信息的完整栈,包括locked <0x...>和waiting to lock <0x...>,这是识别死锁的关键标记 - 用管道过滤关键行:
jstack -l <pid> | grep -A 10 -B 10 "waiting to lock\|java.lang.Thread.State: BLOCKED",快速定位阻塞点 - 注意
Found one Java-level deadlock:这行——JDK 自带检测逻辑,只要发生经典循环等待(Thread A 锁着 L1 等 L2,Thread B 锁着 L2 等 L1),jstack 会明确标出,并列出涉及线程和锁对象
比对多个 jstack 快照,确认死锁是否持续存在
单次 jstack 可能抓到瞬时阻塞,未必是死锁。真正死锁的线程状态不会随时间改变:
- 间隔 5–10 秒连续抓两次:
jstack -l <pid> > jstack_1.log,等几秒再jstack -l <pid> > jstack_2.log - 用
diff jstack_1.log jstack_2.log对比,重点关注java.lang.Thread.State: BLOCKED的线程及其锁地址是否完全一致 - 若两个快照中,相同线程始终持同一把锁、等同一把锁,且无任何进展(比如没有 unlock / notify / run() 后续),基本可断定为死锁
从堆栈里还原锁对象和业务上下文
找到死锁线程后,不能只看“blocked”,要顺藤摸瓜定位代码位置:
- 在 jstack 输出中,找到该线程的栈顶几行(通常是
at xxx.Service.doSomething(xxx.java:123)),这就是锁竞争发生的业务方法 - 向上翻看该线程此前获取了哪些锁(搜索
locked <0x...>),向下看它正在等哪一把(waiting to lock <0x...>) - 若锁对象是自定义对象(如
com.example.OrderLock@7a8b9c),结合代码检查其加锁顺序;若是java.util.concurrent.locks.ReentrantLock,注意lock()调用是否成对、是否在 finally 中释放 - 特别留意日志中是否出现
parking to wait for(说明用了 LockSupport.park,常见于 AQS 实现,也可能是死锁前兆)
整个过程不需要额外工具、不修改应用、不中断服务,三分钟内完成初筛。关键是别一上来就 dump 全量线程,先用 jstat 排除 GC 干扰,再用 jstack -l + grep 锁定目标,最后靠多快照比对和栈帧回溯坐实证据。

















