
简介TDAThread Dump Analyzer是一款专用于Java线程Dump解析与可视化的轻量级分析工具面向需要排查死锁、线程阻塞、锁竞争及CPU消耗等性能问题的开发与运维人员。资源包为zip格式共3个文件包含可直接运行的jar主程序以及分别用于Windows和Linux环境启动的bat、sh脚本整体仅1.3MB无需复杂安装即可使用。目前已有1333人学习下载。工具支持线程状态分布概览、死锁链路自动检测、锁竞争与线程耗时分析、堆栈深度比较并可将分析结果导出为报告配合jstack等命令生成的dump文件能快速定位卡顿、无响应等问题的根因适合在本地故障排查、压测后分析等场景中作为常备辅助工具使用。1. TDA-Thread Dump Analyzer先从一次Java服务卡死说起Java服务在高峰期突然卡住接口超时、CPU满核最直接的一手证据就是线程转储Thread Dump。但要在一份动辄三五百个线程、几百KB的dump文件里找出谁在等锁、谁在空转靠肉眼逐行看会看到怀疑人生。我把 TDAThread Dump Analyzer当作默认的线程转储分析工具tda-bin-2.3.3.zip 解压即用能快速统计线程状态、按 CPU 时间挑出热点线程、把死锁环直接摊开。这篇文章面向需要亲手排查Java故障的后端开发、运维和性能调优的人从怎么采 dump 开始一路拆到怎么用 TDA 定位锁竞争和死锁最后把采样和解析的踩坑经验一次讲清楚。2. 为什么要用 TDA线程转储的格式、信息量与 TDA 的解析逻辑2.1 HotSpot 线程转储里有什么TDA 要处理的信息单元在打开 TDA 之前你得先知道你交给它的是什么。HotSpot 虚拟机用jstack输出的线程转储本质上是一段有固定格式的纯文本。一行线程头跟着一串栈帧中间夹杂锁标记。我截一段常见的样子http-nio-8080-exec-7 #23 daemon prio5 os_prio0 cpu123.45ms elapsed102.35s tid0x00007f9a0c009800 nid0x2a1a waiting on condition [0x00007f9a0a3f9000] java.lang.Thread.State: TIMED_WAITING (sleeping) at java.lang.Thread.sleep(Native Method) at com.demo.Service.doSomething(Service.java:88) - parking to wait for 0x00000000abcd1234 (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject) - locked 0x00000000abcd1234 (a java.util.concurrent.locks.ReentrantLock$NonfairSync)这段文本里真正有价值的信息分成几层。第一层是线程头线程名、线程编号、daemon 标识、优先级、CPU 时间、线程 idtid、十六进制原生线程 idnid、以及当前状态。第二层是java.lang.Thread.StateTDA 就是靠这个字段给线程分类的。第三层是栈帧从栈顶往下读能看出线程此刻的执行路径。第四层是锁标记locked表示这个线程持有某把锁parking to wait for和waiting to lock表示这个线程在等某把锁。TDA 做的事情说穿了就是把这四层信息拆开、归类、再关联。它不需要你手工去数状态也不用你去记哪把锁被哪个线程拿着工具会替你把这些关系建成索引。理解了这一点后面看 TDA 的统计界面就不会一头雾水。2.2 为什么不能靠肉眼和 grep线程 dump 的规模与 TDA 的聚合方式有人会说grep 不就行了grepwaiting to lock能找出所有在等锁的线程再用-A带上几行上下文看线程在等哪把锁。这在 dump 只有几十个线程的时候确实够用但生产环境的线程 dump 往往是三四百个线程每个线程栈深二三十层文件轻松超过 300KB。你手工 grep 出来的只是谁在等而谁持有这把锁这个关键信息在另一个线程的栈里你需要让两份上下文对得上号。锁对象地址是十六进制的一串数字人眼对十六进制地址做关联一次两次还行三十次以上就必然犯错。TDA 的价值在于它把这种关联变成了结构化的数据。它在解析时维护了一个锁表锁对象地址作为 key持有者线程、等待者线程列表都挂在这个 key 下面。你在 TDA 里点开一个锁能直接看到谁拿着它、有哪些线程在排队。同时它会把线程按状态分组统计RUNNABLE、WAITING、TIMED_WAITING、BLOCKED 各多少个一眼就知道当前 dump 是大家都在干活还是全堵在锁上。下面是 grep 和 TDA 处理同一份 dump 的对比分析需求grep 手工处理TDA统计各状态线程数量需多次 grep wc 组合打开文件即显示统计锁持有者与等待者关联手工比对十六进制地址锁表自动关联死锁环识别肉眼对比容易漏自动标记并列出环按 CPU 时间排序无现成手段列表按 CPU 字段排序2.3 获取 TDA 能识别的线程转储jstack、jcmd 与 kill -3 的差异TDA 只是分析端喂给它的 dump 得靠 JVM 自带的命令生成。我一般这样采集# 先用 jps 找到 Java 进程 jps -l # 方式一jstack 带锁信息这是 TDA 最合适的输入 jstack -l 9527 /tmp/dump_$(date %H%M%S).txt # 方式二jcmd效果和 jstack 基本一致JDK 8 以上推荐 jcmd 9527 Thread.print -l /tmp/dump_jcmd_$(date %H%M%S).txt # 方式三kill -3输出到进程的标准输出适合拿不到 jstack 的场景 kill -3 9527命令关键在于-l参数。-l全称是 long mode会在线程栈后面额外输出锁的详细信息。TDA 分析锁竞争和死锁全靠这部分锁信息不带-l的 dump 在 TDA 里打开线程状态和栈帧能看到但锁分析基本是废的。kill -3不需要额外权限但输出写到了进程的 stdout通常在nohup.out里需要自己去截取而且它只输出到 stdout不会自动存成文件。三种方式对比方式是否带锁信息是否阻塞 JVM输出位置jstack -l是阻塞毫秒级指定文件jcmd Thread.print -l是阻塞毫秒级指定文件kill -3是不阻塞进程 stdout还有个细节容易被忽略抓线程 dump 不是抓一次就够。一次 dump 只是某一瞬间的快照线程状态秒级变化。常见做法是每隔 5 到 10 秒连续抓 3 到 5 份再逐份喂给 TDA 看趋势比单份更接近真相。这个我在第 5 章会展开说先按下不表。3. 把 tda-bin-2.3.3.zip 用起来安装、启动与首次分析3.1 解压与启动tda-bin-2.3.3.zip 的目录结构和启动方式拿到tda-bin-2.3.3.zip它是个免编译的二进制发行包解压就能跑。前提是机器上装了 Java 运行时TDA 本身是 Java 写的桌面应用JDK 8 或更高版本都能跑。我习惯把它放在一个固定目录比如~/tools下这样后续采集脚本引用路径时不会乱。# 解压到工具目录 unzip tda-bin-2.3.3.zip -d ~/tools/ # 查看解压后的结构 cd ~/tools/tda-bin-2.3.3 ls -l bin lib # 确认 Java 环境 java -version # 启动 TDALinux/macOS 用脚本Windows 用 tda.bat ./bin/tdabin目录下是启动脚本lib目录下是运行所需的 jar 包。启动后会出现一个 Swing 风格的图形界面左上角是菜单栏和工具栏中间是分析区。如果你的服务器是无桌面环境可以先把 dump 文件下载到本地再在你自己的电脑上启动 TDA 分析这是最顺手的用法。另外我一般建议把 TDA 装在本机而不是服务器上因为它需要图形界面服务器上跑它纯属给自己添堵。3.2 打开第一个 dumpTDA 主界面的布局与信息入口TDA 打开文件的方式很直观File 菜单里的 Open选中你之前抓的 dump 文本文件即可。打开之后界面上有几个区域要分清否则容易不知道看哪里。左侧是线程列表按线程分组展示组内能看到每个线程的名称、状态和 CPU 时间。右侧或上方的统计面板会显示线程总数以及 RUNNABLE、WAITING、TIMED_WAITING、BLOCKED 各占多少。选中左侧某个线程下方会显示该线程的完整栈帧锁信息也在这一带展示。第一次打开一份 dump我建议先看统计面板不看具体线程。统计面板会直接告诉你这个时刻有多少线程在正常执行、多少在等锁、多少在休眠。如果 BLOCKED 数量异常高这份 dump 大概率有锁竞争问题。如果大量线程集中在 WAITING且线程名都带 http 或 pool 字样那基本是线程池耗尽。TDA 还支持按关键字过滤线程名比如输入http-nio只看 Web 容器线程输入pool只看业务线程池。这个过滤在分析线程池耗尽时特别有用能把不相干的 GC 线程和 JVM 内部线程暂时排除掉。3.3 用一份真实 dump 走通最短路从线程统计到栈帧我整理了一份最小步骤照着走一遍就能建立手感。第一步采集。执行jstack -l pid /tmp/dump1.txt。第二步打开。在 TDA 里 File - Open选中 dump1.txt。第三步看统计。确认线程总数和 BLOCKED/WAITING 数量记下来。第四步如果有 BLOCKED 线程在左侧列表按状态排序点开一个 BLOCKED 线程。第五步读栈帧。从栈顶往下读先看最上面三行那是线程此刻真正在执行的动作再往下找waiting to lock或parking to wait for行锁定它等的那把锁地址。第六步回 TDA 的锁表找到这个锁地址看持有者是谁。这六步走完你基本已经能回答两个高频问题这个线程卡在哪它在等谁。剩下的死锁和 CPU 热点是下一章的内容。4. 用 TDA 定位三类高频问题CPU 飙升、锁竞争与死锁4.1 按 CPU 时间排序找到那个吃满核的线程线上 CPU 飙高第一反应是抓线程 dump。TDA 左侧线程列表里有 CPU 时间这一列数据来自 jstack 输出里的cpu123.45ms字段你点列头就能排序。以我的经验CPU 问题分两类。第一类是某个业务线程占满核它的 cpu 值会远远甩开其他线程栈顶通常是重复执行的方法可能是 while 死循环、正则表达式回溯、或者大对象 JSON 序列化。第二类是多个线程一起高 CPU但单个看起来都不突出这种情况要重点看 GC 线程。TDA 里G1 Young RemSet Sampling Task、VM Thread这类名字的线程如果 cpu 值很高说明 JVM 在频繁进行垃圾回收单纯看业务线程栈解决不了问题得配合jstat -gcutil看 GC 频率。这里有一个使用习惯TDA 按 CPU 排序时默认可能把 GC 线程混在业务线程里。我一般会先用过滤框输入线程名关键字分两组看——先看http、pool开头的业务线程再看 GC 相关线程两组都排一遍序避免遗漏。4.2 锁竞争与阻塞WAITING/BLOCKED 线程在等谁锁竞争最常见的现象是线程状态统计里 WAITING 或 BLOCKED 的数量异常而它们等锁的标记在栈里长这样pool-3-thread-12 #41 prio5 os_prio0 cpu0.57ms elapsed582.19s tid0x00007f9a0c183000 nid0x3e7e waiting for monitor entry [0x00007f9a0a0f7000] java.lang.Thread.State: BLOCKED (on object monitor) at com.demo.OrderService.submitOrder(OrderService.java:102) - waiting to lock 0x00000000e1a2b3c4 (a java.lang.Object)在 TDA 里看到 BLOCKED 线程操作顺序是这样的先看它waiting to lock后面的锁地址然后在锁表里找到这个地址锁表会显示持有者线程是谁。这时候判断思路就很清晰了——持有者线程在干什么它在长时间执行一个慢 SQL、在远程调用超时、还是它自己也卡在另一把锁上我处理过最典型的一类问题是数据库连接池耗尽。业务线程全在waiting to lock一个内部锁持有者是池化线程而池化线程卡在获取数据库连接上。这种情况下 TDA 的锁表能清楚显示几十个线程在排队等同一个锁对象问题边界一下就出来了。4.3 死锁检测TDA 如何把环形等待摊开给你看死锁是锁竞争的最极端形态多个线程各自持有一把锁又都在等对方手里的锁形成环。jstack 会在 dump 文件末尾直接打印Found one Java-level deadlock的检测结果TDA 也会在统计面板或线程列表里标记死锁线程。TDA 的优势在于它能帮你把死锁环可视化。你在左侧看到标记为 deadlock 的线程逐个点开栈帧观察它们持有的锁和等待的锁地址很容易看出 A 持锁 1 等锁 2B 持锁 2 等锁 1 的环。下面是个典型死锁对的栈形态thread-A #12 prio5 os_prio0 cpu1.10ms elapsed99.50s tid0x00007f9a0a009000 nid0x2f32 waiting for monitor entry java.lang.Thread.State: BLOCKED (on object monitor) at com.demo.LockService.methodA(LockService.java:45) - waiting to lock 0x00000000f1a2b3c4 (a java.lang.Object) thread-B #13 prio5 os_prio0 cpu1.08ms elapsed99.50s tid0x00007f9a0a00a000 nid0x2f33 waiting for monitor entry java.lang.Thread.State: BLOCKED (on object monitor) at com.demo.LockService.methodB(LockService.java:68) - waiting to lock 0x00000000a1b2c3d4 (a java.lang.Object)只看这两段看不出环你得往上翻前面线程的栈找到 thread-A 持有0x00000000a1b2c3d4、thread-B 持有0x00000000f1a2b3c4才构成完整闭环。TDA 的锁表已经把这个关系统计好了不需要手工翻前后文。不过我要提醒一句TDA 版本不同死锁识别逻辑有一定差异我在第 5 章会讲怎么给它做复核。5. 避坑TDA 分析线程转储的常见误判与排查要点5.1 一次 dump 不能代表持续状态采样频率与对比现象TDA 统计面板显示大量线程处于 TIMED_WAITING判断服务线程假死结果业务其实一切正常。原因线程转储是快照它只记录抓取瞬间的线程状态。线程可能在执行一个耗时 2 秒的 IO 操作抓取时恰好处于 WAITING看起来就像被卡住了。把单次快照当成持续状态是我见过最高频的误判。解决连续采样。我一般间隔 5 秒抓一份连续抓 3 到 5 份逐份用 TDA 打开看统计面板的数量变化。如果某类状态在 3 份里都恒定占比很高才是真问题如果只是某一瞬间异常大概率是正常波动。这个习惯救过我很多次值得养成。5.2 别漏掉 GC 线程和 VM 线程过滤视图会骗人现象TDA 线程列表里业务线程状态看着都正常CPU 时间也不高但服务就是慢。原因TDA 把线程分组展示时JVM 内部线程和 GC 线程有时会收敛在较隐蔽的组里不展开看容易漏。业务线程健康不代表 JVM 健康GC 线程如果 cpu 时间异常高说明内存回收频繁服务整体性能必然受影响。解决在 TDA 的过滤框里输入GC、VM、Sampling等关键字把 JVM 内部线程单独筛出来看 CPU 排序。分析 CPU 问题时这些隐藏角色往往才是主角。5.3 容器场景拿不到正确 pidjstack 报错的真实原因现象在容器里执行jstack 1报错Unable to open socket file或process doesnt exist。原因容器 Java 进程未必是 1 号进程尤其是镜像里同时跑着多个进程时。另外Java 进程和jstack执行者的用户不一致也会导致无法 attach。解决先jps -l或ps -ef | grep java拿到真实 pid再jstack -l 真实pid。如果还是报权限类错误常见做法是改用kill -3 pid它不需要 attach 机制输出到进程标准输出之后在nohup.out或容器日志里取 dump 段。5.4 文件打不开或解析为空编码、参数与 JVM 型号差异现象TDA 打开某个 dump 文件界面空白或提示文件格式无法识别。原因常见有三种。一是采集时没用-l参数锁信息缺失TDA 解析时类型不完整。二是文件被传输工具转成了带 BOM 的编码或换行符被改过导致解析器找不到线程头。三是 dump 来自 IBM J9 虚拟机格式与 HotSpot 差异较大TDA 某些版本支持不完整。解决确认采集命令带-l用file命令检查 dump 文件编码确保是 UTF-8 或 ASCII看到java.lang.Thread.State字段的行存在才说明这是 TDA 熟悉的格式。IBM J9 的 dump 尽量去对应厂商工具分析。5.5 TDA 死锁标记需要复核不是所有环都叫死锁现象TDA 标记了死锁但你把相关线程栈读完发现它们只是运行缓慢并没有互相等待。原因TDA 的死锁检测基于锁的持有和等待关系做图推断如果 dump 里存在多个锁对象互相嵌套而其中某些线程只是等待超时后释放了锁工具可能给出偏高误报。解决拿到 TDA 死锁标记后先看 jstack 输出文件末尾有没有Found one Java-level deadlock字样。这是 JVM 自身的检测结论比工具的推断更可靠。如果两份结论一致再动手排查代码不一致时以 jstack 的断言为准。6. 让 TDA 融入日常排障命令行采集、批量分析与验证习惯6.1 采集、归档与打开一份可重复的线程 dump 脚本TDA 本身是图形工具但采集端可以脚本化。我维护了一个简单脚本出问题时跑一次能拿到按时间戳命名的一组 dump避免手忙脚乱地敲命令#!/bin/bash # 用法: ./collect_tdump.sh pid [采样次数默认3次] PID$1 COUNT${2:-3} mkdir -p tdump_$(date %Y%m%d_%H%M%S) cd tdump_$(date %Y%m%d_%H%M%S) for i in $(seq 1 $COUNT); do jstack -l $PID dump_${i}_$(date %H%M%S).txt sleep 5 done脚本做了三件事按日期建目录归档循环抓取指定次数的 dump每次抓取之间间隔 5 秒。抓完后把这些文件下载到本地逐个用 TDA 打开对比统计面板的数据。我还会顺手抓一份jstat -gcutil的 GC 数据配合 TDA 的线程分析一起判断因为很多线程卡顿的根因在 GC 而不是锁。6.2 让 TDA 当复判工具先自己读栈再用工具验证用 TDA 久了会有一个依赖风险你看问题越来越依赖工具的标记和统计自己的读栈能力反而退化。我给自己定的规矩是每份 dump 先手工做两件事再打开 TDA。第一件事用 grep 统计java.lang.Thread.State的次数分布大概知道 RUNNABLE、WAITING、BLOCKED 各占多少。第二件事翻 dump 末尾找deadlock关键字确认 JVM 是否断言了死锁。这两步成本极低却能在打开 TDA 前建立基线判断。之后 TDA 的作用是验证和深化它统计的数值是否和我的 grep 一致锁表里关联的持有者是否和我在栈帧里读到的一致。用这套流程TDA 标识的死锁我只信一半另一半靠自己的读栈来坐实踩坑率大幅下降。6.3 把 TDA 的判断写进故障报告一个最小验证模板排障结束不等于工作结束报告里得有可复现的数据。我写故障报告时会附一张固定格式的表格填上关键信息项目数据采集时间2024-06-18 14:32:00进程 pid9527线程总数 / RUNNABLE / WAITING / BLOCKED312 / 45 / 201 / 66CPU 消耗最高的线程http-nio-8080-exec-12栈顶为 OrderService.submitOrderJVM deadlock 断言无TDA 死锁标记无结论线程池线程大量阻塞在数据库连接池获取锁上非死锁填这个表的过程就是逼自己验证 TDA 结论的过程。我不止一次发现写着写着发现某个数字和栈帧对不上回头重查结果是采集时漏了-l参数导致锁表不完整。现在我在采集命令里把-l写死就是那次翻车留下的习惯。最后说一件我自己的事。早年间我拿到单份 dump 就下结论报告写线程池泄漏结果开发同事复查发现只是采样瞬间大家都在等待超时重试差点误导了一次发布决策。从那以后我的排障流程固定成了三件套连续采样、grep 打底复核、报告留痕。TDA 是个好工具但工具只负责把数据摊开下结论的还是你自己。希望这套用法对你也有帮助。本文还有配套的精品资源点击获取