资讯动态

jstack实战手册:线程快照、死锁定位与CPU飙升排查

发布时间:2026/10/20 7:20:50 来源:尧图企业网站定制
你有没有遇到过这种情况线上服务突然卡住接口超时报警刷屏日志翻了一圈却什么异常都没打出来。CPU看着正常内存也没报OOM可请求就是处理不完线程池里的任务全部堆积新请求进不来老请求出不去。面对这种“查日志查不出、看监控看不出”的局面JVM 自带的命令行诊断工具就派上用场了而其中最常用、也最容易上手的一个就是 jstack。jstack 是 JDK 自带的一个命令行工具核心功能是生成 Java 进程当前时刻的线程快照thread dump。所谓线程快照就是把 JVM 内部所有线程这一刻的栈信息全部打印出来包括线程名、线程状态、锁信息、调用栈以及当前正在执行的代码位置。拿到这份快照就等于拿到了一张事故现场的全景图很多平时藏在代码里的问题——死锁、线程池耗尽、CPU 飙高、锁竞争——都会在栈帧里现出原形。这篇文章不是简单的 jstack 参数手册而是想把我实际排查问题时的完整思路、看输出时的判断方法、以及踩过的那些坑都讲清楚。适合刚接触 JVM 诊断工具的同学也适合那些用过 jstack 但总感觉看不懂 dump 文件的人。读完你至少能自己做一次完整的线程问题定位而不是抓着一份几百行的 dump 发呆。1. 为什么线上出问题的时候最先想起的是 jstack1.1 jstack 到底是什么JDK 自带的线程快照工具jstack 的全称是 Java Stack Trace从名字就能看出它的定位查看 Java 线程的栈信息。它不需要额外安装只要你机器上有 JDK在 bin 目录下就能找到这个可执行文件。它的工作方式很简单——通过 attach 机制挂到目标 Java 进程上请求 JVM 导出所有线程的当前栈状态然后输出到控制台或文件。从 JVM 诊断工具链的角度看jstack 和 jstat、jmap、jcmd 这几个工具各有分工jstat 看 JVM 内部统计数据GC、类加载、编译情况jmap 看堆内存对象分布和生成堆转储jcmd 是个综合命令入口而 jstack 专门负责线程栈。线程栈回答的问题是“我的线程们此刻在干什么”这正好补上了其他工具够不着的一块运行时行为。我自己的使用感受是jstack 是这几个工具里上手门槛最低的。jstat 的一堆 JVM 参数和 GC 数据要理解半天jmap 的堆转储分析流程又长又重唯独 jstack一条命令跑完输出的就是人能读的文本。只要你能看懂 Java 中线程的运行逻辑就能从这份文本里提取有价值的信息。1.2 什么场景下真正需要 jstack症状与工具匹配很多人在线上出问题时没有第一时间想到 jstack原因是不知道什么症状该用它。我根据实际经验整理了一张对应表遇到下面这类情况优先考虑拿线程快照看看症状表现可能原因首选诊断手段接口长时间不返回请求堆积业务线程阻塞在某个锁或 IO 等待上jstack 看线程状态服务整体“卡死”日志却无异常死锁或线程池全部耗尽jstack 找 deadlock 和等待中的线程CPU 飙升到 100% 但 GC 正常某个线程在死循环或自旋top 定位线程 jstack 看对应栈帧线程数持续增长内存缓慢上涨线程泄漏或有大量线程创建未回收连续多次 jstack 对比线程名变化应用假死无法响应 dump 请求进程已严重异常jstack -F 强制抓取配合其他手段这份表的逻辑是凡是问题表现为“线程不正常”并且你怀疑是阻塞、死锁、自旋、线程泄漏这类运行时行为问题jstack 基本都能派上用场。反过来如果问题是内存溢出、GC 频繁、对象泄漏那优先用 jmap 或 jstat别拿 jstack 硬磕。1.3 使用 jstack 前的第一件事先拿到目标 PID无论你用哪个 JVM 工具第一步都是找到目标 Java 进程的 PID。这一步经常被新手忽略直接输入 jstack 却报错“process not found”其实就是 PID 没找对。推荐用 jps 而不是 ps。jps 是 JDK 自带的专门列出 JVM 进程输出比 ps 干净得多而且能精确识别 Java 进程。jps -l输出示例12345 com.example.order.OrderApplication 23456 sun.tools.jstatd.Jstatd第一列是 PID第二列是主类或 jar 包路径。如果你想看完整的启动参数可以加-v想看进程启动时传入的 main 参数用-m。找到 PID 之后直接执行jstack 12345如果执行时报错说权限不足或无法 attach检查两件事一是当前系统用户是否和启动 Java 进程的用户一致二是 JDK 版本是否和运行时版本匹配。跨用户 attach 基本都会失败这是最常遇到的坑。2. 看懂线程快照jstack 输出里的每一行都在说什么拿到 jstack 输出后很多人的第一反应是“这么多行从哪看起”。别慌本质上线程快照就是一长串线程信息的集合每个线程的信息块格式是固定的看懂一个就能看懂全部。2.1 线程状态的真正含义RUNNABLE 不代表一定在跑JVM 里线程状态分为六种NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED。在 jstack 输出中每种状态都对应一行java.lang.Thread.State: XXX。这里最容易产生误解的是 RUNNABLE。很多初学者以为 RUNNABLE 就是线程正在执行 CPU 指令其实不是。RUNNABLE 只表示线程“可以运行”它可能真的在 CPU 上执行也可能正在等待 IO 完成、等待系统调用返回、等待获取内部锁。比如下面这个栈java.lang.Thread.State: RUNNABLE at java.net.SocketInputStream.socketRead0(Native Method) at java.net.SocketInputStream.socketRead(SocketInputStream.java:116) at java.net.SocketInputStream.read(SocketInputStream.java:171)这绝对不是“线程在干活”它卡在 socket 读数据上也就是等待网络数据返回。所以看 RUNNABLE 状态时一定要结合栈帧顶部的代码位置判断它到底在“真运行”还是“假运行”。我把不同线程状态的处理建议整理成一个表格方便排查时对照线程状态含义排查时怎么理解RUNNABLE正在运行或等待系统资源看栈顶CPU 计算 / 锁自旋 / IO 读都在这BLOCKED等待进入 synchronized 代码块或方法有竞争 monitor锁被其他线程持有WAITING无限期等待如 wait()、join()、park()常见于线程池空闲或条件等待TIMED_WAITING有超时时间的等待sleep、wait(带超时)、join(带超时) 均在此列2.2 线程信息块的字段拆解从线程名到 nid每个线程的信息块开头是这样一行http-nio-8080-exec-10 #25 daemon prio5 os_prio0 cpu12.34ms elapsed123.45s tid0x00007f9c3c04a800 nid0x2a1b runnable [0x00007f9c0b3fe000]逐个拆解http-nio-8080-exec-10是线程名通常由线程工厂或框架指定比如 Tomcat 的工作线程、Netty 的 IO 线程等。#25是 JVM 内部线程编号。daemon表示守护线程非守护线程旁边不会出现这个词。prio5是 Java 优先级os_prio0是操作系统优先级。cpu从高版本 JDK 开始会显示线程累计消耗 CPU 时间短时间内多次采样对比这个值能快速判断哪个线程在“烧 CPU”。tid是 JVM 内部线程 IDnid是操作系统线程 ID排查 CPU 问题时需要和 top 输出对上号的就是 nid。runnable是线程调度状态快照后面的[0x00007f9c0b3fe000]是线程栈基地址。实战中最有用的两个字段是线程名和 nid。线程名告诉你是谁创建的线程nid 让你能结合操作系统工具定位到具体线程。2.3 锁相关的三行标记locked / waiting to lock / waiting on线程快照里和锁相关的输出是排查死锁和竞争的关键。常见的有这几种- locked 0x000000076b5f4a50 (a java.lang.Object) - waiting to lock 0x000000076b5f4a50 (a java.lang.Object) - waiting on 0x000000076b5f4a50 (a java.lang.Object)locked表示线程当前持有了这个对象上的 monitor。synchronized 是锁住对象在 dump 中就会这样显示。waiting to lock表示线程正在尝试进入 synchronized 块但锁被别人持有所以进入 BLOCKED 状态。waiting on表示线程已经持有 monitor并执行了 wait()正在等待其他线程调用 notify()通常对应 WAITING 状态。ReentrantLock 在 dump 中的表现和 synchronized 不同。ReentrantLock 用的是park机制所以等待锁的线程状态通常是 WAITING栈帧中会出现LockSupport.park配合其他标记表示等待某个锁。jstack -l 参数会在线程信息块后面额外输出它持有和等待的锁排查 ReentrantLock 相关问题最好加上 -l。2.4 栈帧部分怎么读从栈顶开始找答案线程状态行下面的从at开始的每一行就是一个栈帧排列顺序是从栈顶到栈底。栈顶是线程当前正在执行的代码栈底是线程的入口方法。排查时我一般从最上面的at行开始看因为那才代表线程当下真正卡住的位置。同样是“什么都不干活”的线程卡在socketRead0和卡在LockSupport.park的排查方向完全不同。前者要去看网络、看下游依赖后者要去看锁的持有者是谁。栈帧会一层层地把调用链路铺开只要你熟悉项目代码看到一个包名和方法名基本能判断出这是业务代码的哪个逻辑分支。3. 三大高频战场死锁、CPU 飙高与线程池被打满jstack 知识点聊完真正有价值的还是实战。我挑三个线上最常遇到、也是 jstack 最能发挥价值的场景完整走一遍排查链路。3.1 死锁定位jstack 自带死锁检测死锁是并发编程里最经典的问题特点是两个或多个线程互相持有锁又互相等待对方释放锁。死锁发生时相关线程都会阻塞但日志往往非常干净。之前处理过一个内部交易系统的问题。A 线程持有账户 A 的锁等待账户 B 的锁B 线程持有账户 B 的锁等待账户 A 的锁。转账接口大批量超时业务日志里只有“超时”字样。执行 jstack 后很快在输出末尾看到Found one Java-level deadlock: thread-transfer-B: waiting to lock monitor 0x00007f9c3c039800 (object 0x000000076b5f4a60, a com.example.Account), which is held by thread-transfer-Ajstack 在 dump 阶段会主动检测死锁并在文件末尾打印死锁列表包含线程名、锁对应对象和持有者。看到这个问题基本就定性了不用再东猜西猜。修复死锁的思路我建议按顺序尝试最简单的是让所有线程按固定顺序获取多个锁从根上消除循环等待如果锁顺序难以统一改用ReentrantLock的tryLock并加超时超时后主动回滚或重试或者把多个锁合并为一个粒度更粗的锁牺牲一点并发度换稳定性。排查死锁时有个小技巧单独执行一次 jstack 能看到死锁快照但如果死锁导致进程已经无响应建议用jstack -F强制抓取否则命令可能挂住。3.2 CPU 飙高从 top 到 nid 再到栈帧的一套固定打法CPU 飙高是线上最紧急的故障之一。遇到这种情况我第一反应不是马上 jstack而是先拿操作系统的工具定位到底是哪个线程在消耗 CPU。完整流程是这样的第一步用 top 查看进程级 CPU 占用找到 Java 进程的 PIDtop -c第二步查看这个进程内哪些线程 CPU 占用最高top -Hp 12345第三步把占用最高的线程的十进制 ID 转成十六进制printf %x\n 6789输出可能是1a85这个十六进制值就是 jstack 输出里的 nid。第四步执行 jstack 抓取线程快照然后在输出中搜索 nidjstack 12345 /tmp/thread_dump.log grep -A 20 nid0x1a85 /tmp/thread_dump.log这样就能看到到底是哪段业务代码在烧 CPU。举个例子。有个订单扣减服务 CPU 频繁飙到接近 100%看着像是在做大量计算。按上面的流程抓了线程栈发现栈帧一直停留在某个 JSON 工具类的一个方法上而且连续多次采样都是同一位置。顺着调用链往下一层发现是某个配置表在每次请求时重复解析、重复构建对象加上循环嵌套一个请求可能触发几千次重复计算。问题定位到之后把解析结果挪到启动时缓存CPU 立刻降下来了。这套打法的核心思想是jstack 负责给“线程在干什么”提供证据top 负责缩小范围两者配合才能避免拿着 JVM 工具在几百个线程里大海捞针。CPU 高但 top 看到的却是 GC 线程那就要换方向看 GC 日志和堆内存了。3.3 线程池被打满从 WAITING 堆叠与线程名找线索线程池耗尽的问题表现通常是接口全部超时线程数达到最大值队列堆积。jstack 在这类问题里的作用是告诉你这些线程到底“卡”在哪一步。先看线程状态分布。如果大量业务线程处于 WAITING 或 TIMED_WAITING并且栈帧停在队列 take/poll 上那说明线程池本身没任务可干是空闲等待这类状态反而是正常的。真正要警惕的是两种异常形态。第一种是大量 RUNNABLE 线程长时间停留在同一个业务代码方法上。这说明并发请求扎堆在同一条路径上可能是热点资源竞争、慢 SQL 同步调用、或者是下游接口超时导致线程集体等待。第二种是大量 BLOCKED 线程这几乎必然对应出现了一个被多个线程竞争的锁比如某个全局锁、某个单例对象的同步块、某个静态集合。线程名能帮我们快速区分线程池来源。比如 Tomcat 的http-nio-8080-exec-开头的线程Netty 的nioEventLoopGroup线程自定义的business-thread-线程。看到线程名后再沿着栈帧回到业务入口就能判断是调用上游超时、数据库等待还是锁竞争。排查线程池问题时我强烈建议多采样几次每次间隔 3 到 5 秒把多份 dump 放一起看。单份 dump 可能刚好抓到线程池空闲的瞬间导致误判为“没有任务在跑”连续多份才能看出线程状态的动态变化。3.4 连续采样瞬时异常往往需要两份以上的 dump 对照有人问我一份 dump 不够吗说实话对于死锁这类静态问题一份就够但对于“线程越来越慢”“CPU 间歇性飙高”“请求随机超时”这类动态问题一份 dump 往往只能拍到某个瞬间抓不到问题全貌。连续采样的方法是隔 3 到 5 秒分别抓一份 dump连续抓 5 到 10 份。for i in {1..5}; do jstack -l 12345 /tmp/dump_$i.txt sleep 5 done然后分析每份 dump 里线程状态分布的差异RUNNABLE 比例是不是持续上升WAITING 线程的栈帧位置是否一致某个线程是否每次都出现在同一行代码上如果某个栈帧连续出现在多份 dump 中基本可以断定它就是一个热点值得深挖。另外结合第一次和最后一次 dump 之间的线程数量差异还能判断线程泄漏。比如第一次 200 个业务线程最后一次 300 个而且新增线程名都是同一种前缀、栈都停在同一个位置说明每次请求都在创建新的线程并且没有释放。4. 参数与组合拳-F/-l/-m 以及和 jstat、jmap 的搭配jstack 虽然基础用法简单但几个参数选得对不对直接决定抓出来的内容有没有价值。4.1 jstack 的四个关键参数怎么选常用参数就这几个参数作用使用建议无参数普通线程快照不含锁的详细信息日常优先使用-l长列表打印锁的额外信息拥有的锁和等待的锁排查死锁、锁竞争时必须加-F强制 dump进程无响应时使用谨慎使用可能影响生产进程-m混合模式同时打印 Java 栈和 native 栈排查系统调用、JNI 问题时使用-l是我在大多数生产场景下默认加上的参数。不加 -l 的话ReentrantLock 等信息显示不完整排查锁问题时少了一半线索。不过 -l 会让 dump 文件变大处理大数据量时要留意。-F是强制模式它的原理是使用 JVM 的底层调试接口直接读取线程状态即使目标进程陷入类似死循环、完全无法响应正常 dump 请求的状态也能抓到信息。但强制模式是对运行中 JVM 的一种侵入极端情况下可能导致被诊断进程不稳定所以能不用就不用用了之后要格外关注目标进程状态。-m通常用的比较少。它会把 native 层的栈帧也导出来比如线程卡在原生 socket 读取或 JNI 调用中时能看到更底层的调用。一般排查 Java 层问题用不到但是做底层系统排查时能派上用场。4.2 输出重定向与分析dump 文件别直接肉眼扫jstack 输出的内容可能很长尤其线程数多的大应用一份 dump 可能上千行。建议每次都重定向到文件再用工具辅助分析。jstack -l 12345 /tmp/thread_dump.log文件生成后先做三件事第一看文件头部的时间戳确认采样时间点第二用 grep 统计各线程状态的数量第三搜索deadlock关键字确认是否存在死锁。grep -c java.lang.Thread.State: RUNNABLE /tmp/thread_dump.log grep -c java.lang.Thread.State: BLOCKED /tmp/thread_dump.log grep -c java.lang.Thread.State: WAITING /tmp/thread_dump.log统计出状态分布之后如果 RUNNABLE 为主重点看它们卡在哪个栈帧如果 BLOCKED 数量多去查锁信息如果 WAITING 占比极高去看是线程池空闲还是全部阻塞在某个调用上。4.3 和 jstat、jmap、jcmd 的组合判断jstack 只负责线程状态但线上问题往往是多维度叠加的只靠一份线程快照就给结论不够稳妥。我习惯把它和另外几个工具组合使用。如果 jstack 显示线程大多 RUNNABLE但 top 却看到 CPU 占用率低说明线程可能卡在 IO 等待上这个时候配合 jstat 看 GC 数据意义不大更应该看网络和下游系统。如果 top 显示 CPU 大部分消耗在 GC 线程上jstack 里也看到大量G1或Parallel相关线程活跃这时候问题很可能在堆内存上应该用 jstat -gc 看 GC 频率再用 jmap -heap 或 jmap -dump 看对象分布。如果 jstack 里能明显看到某个线程反复出现在自定义的本地方法调用上可以用 jcmdThread.print -l替代 jstack 再试一次。jcmd 是较新版本 JDK 的综合工具输出的线程信息和 jstack 基本一致但它在某些环境下对进程的侵入更小。jcmd 12345 Thread.print -l /tmp/thread_dump.log诊断工具之间不是互相替代的关系而是互相印证的关系。线程栈给了线索内存和 GC 数据给背景操作系统工具给事实串起来才是完整的故事。5. 那些容易被忽略的坑容器、大堆与采样姿势jstack 用起来不难但坑是真不少。我整理了几类高频问题都是实操中很容易踩中的。5.1 容器环境执行 jstack 的坑容器化部署已经很普遍但在容器里执行 jstack 有件容易被忽视的事容器内看到的 PID 和宿主机上的 PID 不是同一个命名空间。如果你习惯先在宿主机上 top 找到了 Java 进程 PID再进容器执行 jstack PID很可能提示找不到进程。正确做法是先进容器再在容器内部用 jps 找 PID然后执行 jstack。另外很多精简容器镜像只装了 JRE 或者裁剪过 JDKjstack 可能根本不存在这时候要么换用自带诊断功能的镜像要么通过挂载方式引入 JDK 工具要么直接用 jcmd 代替。容器场景还有一个隐蔽问题内存限制。如果容器内存设置过小执行 jstack 时 JVM 需要把线程状态序列化出来可能临时占用较多内存个别极端情况下会触发 OOM Killer。所以容器内执行诊断命令时保证容器有足够的空闲内存会安全很多。5.2 dump 大文件线程数多时的数据处理一个 JVM 里几百个线程并不罕见如果线程名长、栈深、还开了 -l一份 dump 很容易超过几万行。这种文件直接肉眼读效率太低我一般这样处理先按线程名分组统计同一类线程的数量。比如 Tomcat 的工作线程是否一致地停在同一个方法上Netty 的 IO 线程是否全部阻塞在某个读写操作里。整理出线程名和数量分布再挑代表性的线程看完整栈。另外把多次 dump 放在同一个文件里时建议在每次 jstack 输出之间加一行明显分隔方便按时间点切分。执行循环采集时可以用我前面写的 for 循环把每次结果单独存文件文件名里带上序号事后逐一比对。5.3 jstack 本身对目标进程的影响别在生产环境放肆抓jstack 需要 attach 到目标 JVMattach 的过程会触发 JVM 的某些内部操作比如线程转储时可能短暂停顿。大多数情况下这个停顿只有几十到几百毫秒对业务影响可忽略。但如果目标 JVM 本身就处于高负载、内存极其紧张、或者堆很大的状态下抓 dump 的副作用会被放大。以前遇到过一个大堆应用内存 30 GB 以上线程数近千执行 jstack 时目标进程出现秒级停顿直接导致监控告警。所以操作生产环境时我有一条原则先评估影响再动手抓。真要在高峰期抓尽量用低侵入的方式并且一次只抓一份不要连环执行。5.4 JDK 版本差异导致的输出变化不同 JDK 版本的 jstack 输出格式有点差异。比如较老的 JDK 8 某版本里os_prio可能不显示较新的 JDK 17 里线程状态前会多出一些 GC 相关的线程信息。处理 dump 时要用对应版本的工具否则可能出现 attach 失败或输出格式不兼容的情况。还有一点如果目标进程是较新 JDK 编译的但系统的默认 jstack 是旧版 JDK执行时大概率会报版本不匹配。解决办法是直接用目标 JDK 目录下的 jstack或者在调用时加上完整路径。/usr/local/java/bin/jstack -l 12345版本问题最容易在“开发机器上顺手执行一条 jstack”的时候踩中所以脚本里显式指定 JDK 路径是个好习惯。5.5 采样的姿势单次 dump 的误判风险我见过有人拿一份 dump 看到“所有线程都在 WAITING”就断定线程池被打满了。结果是线程池空闲那其实只是业务低峰期的正常状态。单次抓取最大的风险就是过度解读。正确姿势是结合时间点、业务流量、监控指标一起看。如果是高峰期的 dump 中线程大量 BLOCKED可信度很高如果是凌晨低峰期的 dump 里线程都在等待什么都不说明。另外尽量在故障发生过程中抓取故障结束后抓的 dump 往往找不到问题线索。还有个容易误判的场景是自旋锁。现代 JVM 对 synchronized 做了自旋优化线程在快照里显示为 RUNNABLE 且栈帧停留在AbstractQueuedSynchronizer或synchronized相关方法实际上它可能是在自旋等待锁而不是真的在忙业务逻辑。区分办法是连续采样看 CPU 时间如果线程 cpu 字段持续增加说明确实在自旋消耗 CPU。5.6 最后分享一个抓取技巧线程 CPU 时间的对比从 JDK 8 某个版本开始部分平台的 jstack 输出会在线程信息里显示cpu字段它表示线程累计使用的 CPU 时间。连续抓两份 dump比较同一个线程的 cpu 值增长幅度能非常直观地判断谁是真正的 CPU 大户。比如一次故障中某线程第一次采样 cpu500ms第二次采样 cpu3200ms说明两次采样之间它消耗了大量 CPU结合它的栈帧就能确认是不是“热点线程”。这个技巧我在很多场合用过比单纯看线程状态更有说服力。jstack 这个工具看起来简单但真正把它用好需要对 JVM 线程模型有基本理解也需要在实战中积累判断经验。遇到问题的时候别急着盲目抓 dump先想清楚症状、选对参数、连续采样再结合其他工具交叉印证。这样一套流程下来绝大多数线上线程问题都能找到方向。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑