
1. 从一次线上接口超时说起为什么需要 jstack线上服务突然出现接口大面积超时日志里没有任何异常堆栈CPU 使用率也不算高但请求就是卡在那里不动。这种场景相信不少后端同学都遇到过。第一次碰到的时候我盯着监控面板看了半天完全找不到方向——没有报错、没有 OOM、没有明显的资源瓶颈问题像是凭空出现的。后来一位经验更丰富的同事提醒我先抓一份线程 dump 看看。那是我第一次真正用上jstack也是第一次意识到JVM 里那些看不见的线程状态才是排查这类问题的关键入口。jstack是 JDK 自带的一个命令行工具作用是打印指定 Java 进程的线程快照也就是我们常说的 thread dump。这份快照里包含了 JVM 中每一个线程在某一时刻的调用栈、线程状态、锁持有情况等核心信息。它解决的核心问题是当程序卡住但又不报错时帮你把每个线程正在干什么完整地拍下来让你能顺着调用栈找到阻塞点。适合所有做 Java 后端开发、运维、性能调优的同学尤其是需要处理线上问题的一线工程师。哪怕你平时只写业务代码掌握jstack也能让你在排查死锁、线程池耗尽、接口假死这类问题时少走很多弯路。这篇文章不会只给你一条命令就完事而是把jstack背后的线程模型、输出内容的逐行解读、不同场景下的抓取策略、以及我踩过的那些坑完整地讲一遍。读完你至少能做到看到一份 thread dump 不慌知道从哪里开始看能定位到具体是哪一行代码、哪一把锁出了问题。2. jstack 到底抓了什么线程模型与 dump 的本质2.1 JVM 线程与操作系统线程的映射关系要理解jstack的输出得先搞清楚 JVM 里的线程到底是什么。在主流 JVM 实现中一个 Java 线程通常对应一个操作系统原生线程在 Linux 上就是 pthread。JVM 本身并不模拟线程调度而是把线程的调度权交给操作系统内核。这意味着jstack抓到的每一个线程背后都对应着操作系统里一个真实存在的执行单元。这个映射关系很重要因为它决定了jstack能给你什么、不能给你什么。它能给你的是 Java 层面的调用栈也就是从某个入口方法一路调用到当前正在执行的方法的完整链路。它不能直接给你的是操作系统层面的调度信息比如这个线程被分配了多少 CPU 时间片。所以jstack通常要和top -Hp、pidstat这类系统级工具配合使用才能形成完整的排查链路。还有一个细节值得注意JVM 内部线程比如 GC 线程、JIT 编译线程也会出现在 dump 里。这些线程的名字通常带有明确的标识比如GC task thread、C2 CompilerThread。排查业务问题时这些线程一般可以先跳过重点看那些名字和你业务相关的线程比如 Tomcat 的http-nio-8080-exec-*、Dubbo 的DubboServerHandler-*。2.2 thread dump 里到底记录了哪些字段一份标准的 thread dump每个线程块大致包含这些信息线程名称如http-nio-8080-exec-5这是定位线程归属的第一线索。线程优先级与 JVM 内部标识如prio5 tid0x00007f... nid0x5a3c其中nid是操作系统层面的线程 ID和top -Hp看到的十六进制线程号能对上。线程状态如RUNNABLE、BLOCKED、WAITING、TIMED_WAITING这是判断线程卡在哪的核心依据。调用栈从栈顶到栈底的完整方法调用链包含类名、方法名、行号如果编译时保留了调试信息。锁信息如果线程涉及锁会显示waiting to lock、locked等字样以及锁对象的地址。这些字段组合起来就构成了一张线程此刻在做什么的完整快照。关键在于单看一个线程往往看不出问题必须把多个线程放在一起对比才能发现死锁、锁竞争、线程池耗尽这类全局性问题。2.3 为什么 dump 是某一时刻的快照jstack抓取的是瞬时状态这一点经常被忽略。JVM 在抓取时会进入一个安全点safepoint暂停所有线程然后逐个记录它们的调用栈记录完再恢复。所以一份 dump 反映的是某一个精确时刻的状态而不是一段时间内的平均情况。这个特性带来两个实际影响。第一如果问题不是持续性的而是间歇性的单次 dump 可能刚好抓到正常的时刻什么都看不出来。这时候需要间隔性地多抓几次比如每隔 5 秒抓一次连续抓 3 到 5 次对比线程状态的变化。第二抓取动作本身会让 JVM 短暂停顿虽然通常只有几十毫秒但在高并发场景下频繁抓取仍可能造成可感知的影响所以生产环境抓 dump 要控制频率。3. 动手抓一份 dump命令参数与实操细节3.1 最基础的用法与进程定位最基础的命令形式是jstack pid其中pid是目标 Java 进程的进程号。获取 pid 的常见方式有两种# 方式一通过 jps 列出所有 Java 进程 jps -l # 方式二通过 ps 结合关键字过滤 ps -ef | grep javajps是 JDK 自带的工具输出更干净会直接列出 Java 进程的 pid 和主类名。我个人的习惯是优先用jps -l因为它能直接告诉你每个进程对应的是哪个应用避免在多实例部署时抓错进程。ps的方式更通用但需要自己从一长串启动参数里辨认目标进程。注意jstack必须由和目标进程相同用户身份、或拥有足够权限的用户执行。如果目标进程是其他用户启动的普通用户执行会报 Unable to open socket file 之类的错误。这种情况下需要用sudo -u 目标用户切换身份而不是简单加sudo。3.2 常用参数逐个拆解jstack的参数不多但每一个都有明确用途参数作用适用场景-l额外打印锁的附加信息排查死锁、锁竞争时必加-F强制抓取进程无响应时使用进程 hang 死、普通方式抓不到时-m同时打印 native 栈涉及 JNI 调用、native 方法阻塞时-h打印帮助信息忘记参数时快速查阅-l是最常用的增强参数。不加-l时dump 里只会显示线程是否持有锁加上-l后会额外显示锁对象的类信息和持有者排查死锁时信息量差别很大。-F是最后手段当进程已经卡到无法响应正常的 dump 请求时用-F可以强制抓取但代价是抓到的信息可能不完整而且强制抓取本身有一定风险非必要不用。3.3 把 dump 输出到文件而不是刷屏直接执行jstack pid会把内容全部打印到终端几百上千个线程的 dump 能刷好几屏根本没法看。正确做法是重定向到文件jstack -l pid /tmp/dump_$(date %Y%m%d_%H%M%S).txt用时间戳命名文件是个好习惯尤其是需要连续抓多份做对比的时候文件名自带时间信息省得自己记。如果目标机器磁盘空间紧张也可以直接通过管道传给grep做初步过滤但我不建议一上来就过滤因为过滤容易漏掉关键上下文最好先完整保存再在本地慢慢分析。3.4 连续抓取应对间歇性问题对于间歇性卡顿单次 dump 往往不够。我常用的做法是写一个简单的循环for i in 1 2 3 4 5; do jstack -l pid /tmp/dump_$i.txt sleep 5 done这样每隔 5 秒抓一次连续抓 5 次覆盖大约 25 秒的时间窗口。如果问题表现为每隔一段时间卡一下这组 dump 里大概率能抓到卡顿时刻的状态。抓完之后重点对比几次 dump 中同一个线程的调用栈有没有变化——如果某个线程在多次 dump 中都停在同一个位置那基本可以确定它就是瓶颈所在。提示抓取间隔不宜过短。间隔太短几次 dump 的状态几乎一样起不到对比作用间隔太长又可能错过问题窗口。5 到 10 秒是比较稳妥的区间具体可以根据问题的复现频率调整。4. 读懂 dump线程状态与调用栈的逐行解读4.1 五种线程状态分别意味着什么Java 线程状态在 dump 里以大写英文呈现理解它们是解读 dump 的第一步RUNNABLE线程正在运行或准备运行。注意处于 RUNNABLE 不代表一定在消耗 CPU它也可能在等待网络 IO因为 Java 层面无法区分等 CPU和等 IO。BLOCKED线程在等待获取一个监视器锁synchronized说明发生了锁竞争。WAITING线程在无限期等待通常是在调用Object.wait()、Thread.join()或LockSupport.park()。TIMED_WAITING线程在限时等待比如Thread.sleep()、带超时的wait()。TERMINATED线程已结束dump 里一般看不到。排查时大量线程处于 BLOCKED 通常指向锁竞争大量线程处于 WAITING 且调用栈指向线程池的take()方法往往是线程池空闲的正常表现不用紧张。关键是要结合调用栈判断不能只看状态。4.2 一个真实线程块的完整解剖下面是一个典型的线程块为保护隐私类名做了脱敏处理http-nio-8080-exec-5 #35 daemon prio5 os_prio0 tid0x00007f8a4c1a8000 nid0x5a3c waiting on condition [0x00007f8a3b7e0000] java.lang.Thread.State: WAITING (parking) at sun.misc.Unsafe.park(Native Method) - parking to wait for 0x00000000f0a1b2c0 (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject) at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175) at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039) at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:442) at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1074) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1134) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at java.lang.Thread.run(Thread.java:748)逐行看第一行给出了线程名http-nio-8080-exec-5说明这是 Tomcat 处理 HTTP 请求的工作线程nid0x5a3c是操作系统线程号可以和top -Hp的输出对应waiting on condition表示它在等待某个条件。第二行明确状态是WAITING (parking)。调用栈从下往上看最终停在LinkedBlockingQueue.take()这是线程池在等待新任务时的正常阻塞。所以这个线程块是健康的不用管它。4.3 从调用栈定位到具体代码行调用栈里最有价值的信息是行号。比如看到这样一行at com.example.service.OrderService.calculatePrice(OrderService.java:128)就能直接定位到OrderService第 128 行。前提是编译时保留了调试信息默认的javac编译是保留的但如果用了-g:none就会丢失行号。如果发现 dump 里只有方法名没有行号就要检查构建配置是不是把调试信息关掉了。定位到行号之后再结合那一行的代码逻辑判断是在等锁、在等 IO、还是在做耗时计算。这一步是jstack排查的核心价值——它把程序卡住了这种模糊描述变成了卡在第 128 行的这个锁上这种精确结论。5. 三类高频问题的排查链路5.1 死锁如何从 dump 里一眼认出死锁是jstack最擅长的场景。当两个或多个线程互相持有对方需要的锁时JVM 能自动检测到并在 dump 末尾直接给出提示Found one Java-level deadlock: Thread-1: waiting to lock monitor 0x00007f8a4c0a3b00 (object 0x00000000f0a1b2c0, a java.lang.Object), which is held by Thread-0 Thread-0: waiting to lock monitor 0x00007f8a4c0a3b40 (object 0x00000000f0a1b2d0, a java.lang.Object), which is held by Thread-1看到Found one Java-level deadlock这行字基本就实锤了。下面会清楚列出哪个线程在等哪把锁、这把锁被谁持有形成闭环。排查死锁时-l参数一定要加否则可能看不到完整的锁持有关系。我遇到过一次死锁两个线程分别按不同顺序获取两把锁代码里看起来完全正常但并发一上来就互相卡死。dump 一抓JVM 直接告诉我死锁在哪两个对象上顺着对象地址找到对应的代码问题一目了然。如果没有jstack这种问题靠读代码可能要排查很久。5.2 线程池耗尽从线程名和数量入手线程池耗尽的表现是接口响应越来越慢最后完全无响应但 CPU 和内存都正常。这时候抓 dump会看到大量业务线程都处于 WAITING 或 TIMED_WAITING 状态且调用栈都指向同一个下游依赖。排查思路是这样的先数一数 dump 里业务线程的总数和线程池配置的最大线程数对比。如果已经打满再看这些线程都卡在哪里。常见原因是下游接口响应慢导致线程池里的线程全被占住新请求进不来。这时候调用栈会指向发起下游调用的那一行比如某个 HTTP 客户端的execute()方法。解决方向有两个一是给下游调用加超时避免线程被无限期占住二是调整线程池参数但这个要谨慎盲目调大只是把问题推迟治标不治本。5.3 CPU 飙高jstack 与 top 的配合打法CPU 飙高时jstack单独用效果有限需要和top配合。完整链路是用top找到 CPU 最高的 Java 进程 pid。用top -Hp pid找到该进程内 CPU 最高的线程记下它的线程号十进制。把十进制线程号转成十六进制printf %x\n 线程号。在jstack的输出里搜索这个十六进制值对应nid字段定位到具体线程。看这个线程的调用栈找到消耗 CPU 的代码位置。这套打法我用了很多次屡试不爽。有一次线上 CPU 突然打满用这个方法定位到一个正则表达式在特定输入下发生了灾难性回溯调用栈直接指向那一行Pattern.matcher()。如果没有jstack光看代码根本想不到是正则的问题。6. 那些文档里不会写的坑6.1 抓不到 dump 的几种情况和应对最常见的是权限问题前面提过不再重复。另一种情况是进程已经卡到无法响应 dump 请求普通jstack会超时或报错这时候才轮到-F出场。但-F有个坑它通过操作系统的信号机制强制抓取抓到的调用栈可能缺少行号甚至部分线程信息缺失。所以-F是有总比没有强的选择不能指望它给出完美结果。还有一种情况是容器环境。在容器里跑 Java 应用时jstack需要能访问到目标进程的 socket 文件如果容器权限配置过严可能抓不到。这时候要么调整容器权限要么在容器内以正确用户执行。6.2 dump 文件太大怎么高效分析生产环境动辄几百个线程dump 文件几千行肉眼扫根本不现实。我的做法是先用grep做粗筛# 统计各状态线程数量 grep java.lang.Thread.State dump.txt | sort | uniq -c # 找出所有 BLOCKED 的线程 grep -A 20 BLOCKED dump.txt # 搜索特定线程名 grep -A 30 http-nio-8080-exec dump.txt先看状态分布如果 BLOCKED 数量异常多就往锁竞争方向查如果 WAITING 集中在某几个调用栈就往依赖阻塞方向查。粗筛之后再针对可疑线程细看效率比从头读到尾高得多。6.3 抓取时机比抓取本身更重要这一点是我踩坑最多的地方。刚开始用jstack时我总是在问题已经过去之后才去抓结果 dump 里一切正常什么都看不出来。后来才明白dump 是瞬时快照必须在问题正在发生的那一刻抓才有意义。所以现在我的习惯是一旦监控告警第一时间抓 dump而不是先去翻日志。日志可以慢慢看但 dump 的时机窗口稍纵即逝。对于可复现的问题可以在测试环境反复触发、反复抓取积累多份样本对比。对于不可复现的偶发问题可以考虑在应用里埋一个手动触发 dump的入口出问题时一键抓取避免错过窗口。7. 把 jstack 用成肌肉记忆jstack这个工具命令本身简单到只有几个参数但真正用好它靠的是对线程模型的理解、对 dump 内容的熟悉以及大量实战积累出来的排查直觉。我现在的习惯是只要线上出现卡住但不报错的问题第一反应就是抓 dump而不是先怀疑代码逻辑。因为 dump 给出的信息是客观的、精确的它不会骗你能帮你快速排除掉大量猜测。如果你还没怎么用过jstack建议找个测试环境写一段故意制造死锁或线程阻塞的代码亲手抓几份 dump 看看长什么样。看多了那些线程状态和调用栈就会变成一种直觉出问题时一眼就能看出哪里不对劲。这种直觉是任何文档都教不会的只能靠一次次实操喂出来。最后分享一个小技巧把常用的抓取命令做成一个 shell 脚本带上时间戳、自动压缩、自动清理旧文件出问题时一条命令搞定省得手忙脚乱敲错参数。排查问题的时候少一分慌乱就多一分找到根因的机会。