found 1 deadlock.即确认死锁,jstack -l 输出末尾明确标出涉事线程、持有锁与等待锁地址,匹配locked 和waiting to lock 即可定位,缺-l参数则无法显示锁信息。

直接看 jstack 输出里有没有 Found 1 deadlock
有就说明 JVM 已经检测到死锁,jstack 会在输出末尾明确标出所有卷入死锁的线程、它们持有的锁和正在等待的锁。关键不是“怎么分析”,而是“别跳过最后一段”——很多人只扫前面的线程堆栈,漏掉这行字,白查半小时。
常见错误现象:jstack -l <pid></pid> 输出很长,但搜索 deadlock 发现没结果,误以为没死锁。其实可能只是 JVM 检测不到(比如非 synchronized 锁、ReentrantLock 未用 lockInterruptibly 或未开启公平模式),或死锁刚发生还没被定时检测线程捕获(默认 30 秒轮询一次)。
- 必须加
-l参数,否则不显示锁信息,无法判断等待关系 - 如果应用用了
java.util.concurrent.locks.ReentrantLock,需确认是否调用了lock.getHoldCount()或启用了-XX:+PrintConcurrentLocks(JDK 8 及以前) - 死锁不一定表现为“卡死”,有时是部分请求超时、数据库连接池耗尽,本质仍是线程在等锁
CPU 100% 时优先筛出 runnable 状态且无阻塞的线程
jstack 中状态为 RUNNABLE 并且堆栈末尾没有 parking to wait for、waiting on、sleeping 的线程,最可能是 CPU 消耗源。这类线程正在执行纯计算逻辑,比如无限循环、正则回溯、JSON 序列化大对象、未设 limit 的 Stream.collect。
使用场景:用 top -H -p <pid></pid> 找到高 CPU 的线程 tid(十进制),转成十六进制,再在 jstack 输出里搜 nid=0x<hex></hex> —— 这比通读全部线程快得多。
- Linux 下
printf "%x\n" <tid></tid>快速转十六进制 - 注意区分
java.lang.Thread.State: RUNNABLE和底层 OS 状态;前者可能实际在wait()或 I/O,要看堆栈最后一行是否真在执行 Java 字节码 - 典型陷阱:G1 GC 导致的 CPU 高,此时
jstack里会看到大量GCTaskThread或ConcurrentMarkThread,这不是业务代码问题
定位具体代码行数:重点看 at 行 + 行号 + 是否内联
jstack 每个线程堆栈里以 at 开头的行就是调用点,格式是 at com.example.Service.doWork(Service.java:42)。行号真实有效,前提是编译时没关 debug info(即 javac 用了 -g,Maven 默认开启)。
使用固定的 Next.js/Tailwind/Prisma 技术栈构建并记录独立 SaaS 项目,仅将 FastAPI 用于 Python 工作负载,并遵循严格的架构规范。
容易踩的坑:Spring AOP、Lombok、GraalVM native image 会导致行号偏移或消失。例如 @SneakyThrows 插入的 try-catch 块会让实际抛异常位置和堆栈显示不一致;lombok.Generated 注解方法可能不带行号。
- 确认 class 文件含调试信息:
javap -l <class> | grep LineNumberTable</class> - 若行号是
Unknown Source,检查是否用了-Xcompile、ProGuard、或 JDK 17+ 的--strip-debug - 内联优化(如
-XX:+Inline)不会抹掉行号,但会让堆栈变浅,需结合-XX:+PrintInlining日志交叉验证
单次 jstack 不够?要连续采样比对线程状态变化
死锁和 CPU 飙升都是动态现象。一次 jstack 只能拍快照,可能刚好错过临界点。真正有效的做法是间隔 2–3 秒连打 3–5 次,用 grep -A 10 "nid=0x[0-9a-f]\+" *.log 提取目标线程,看它是不是持续停留在同一行、同一锁、同一循环体内。
性能影响很小:jstack 是 JVM 内置诊断命令,全程走 safepoint,对运行中应用的影响远小于一次 full GC。
- 避免用
jstack -F(强制模式),它会挂起所有线程更久,可能让问题暂时“消失” - 生产环境建议配合
async-profiler采样:它不依赖 safepoint,能抓到 native 方法和 JIT 编译后的热点行 - 如果多次采样都看到某线程在
String.replaceAll或Pattern.compile,基本可锁定是正则灾难(catastrophic backtracking)
真正难的不是找到那一行代码,而是确认那行代码在什么数据输入下会触发死锁条件或进入无限循环——jstack 给你的是“在哪里停住了”,不是“为什么停住”。需要结合日志、监控、甚至复现时加 -XX:+TraceClassLoading 看类加载顺序。










