多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Tomcat假死问题深度排查:从线程死锁到内存泄漏的实战指南

Tomcat假死问题深度排查:从线程死锁到内存泄漏的实战指南 1. 问题现象与初步感知什么是Tomcat“假死”在线上服务运维中Tomcat的“假死”状态绝对是一个让人血压飙升的经典问题。它不像服务彻底崩溃那样干脆利落会留下明确的错误日志和进程退出的信号。恰恰相反从外部监控看Tomcat进程依然健在ps命令或者podman ps如果你在用容器显示状态为“Up”可能已经运行了“18 minutes ago”甚至更久。但当你尝试访问应用时浏览器却一直转圈最终超时或者健康检查接口比如/actuator/health返回失败。这种“进程活着但服务不响应”的尴尬局面就是我们常说的“假死”。我遇到过最典型的一次是一个核心交易服务在凌晨流量低谷时一切正常但一到早高峰响应时间曲线就像坐上了火箭直冲云霄然后彻底平直——没有任何新的成功响应了。登录服务器curl localhost:8080直接卡住但Tomcat的进程ID还在jstack命令甚至能打出线程栈可业务就是不动了。这种问题排查起来就像给一个还有心跳和呼吸但对外界刺激毫无反应的病人做诊断需要一套系统性的“体检”流程。2. 系统性排查思路与工具箱准备面对假死切忌无头苍蝇似的乱试。一个高效的排查思路应该像老中医的“望闻问切”由外而内层层递进。首先你需要准备好你的“手术刀”工具箱。对于Java应用以下几把刀是必备的JDK命令行工具这是最基础也是最强大的原生工具集。确保你的服务器上安装了与应用匹配的JDK注意jdk和tomcat的版本有要求吗通常建议使用Tomcat官方推荐的兼容版本避免已知Bug。jps或ps -ef | grep java快速定位Java进程PID。jstack pid获取Java进程的线程转储Thread Dump这是分析线程状态的核心依据。通常需要连续打2-3次间隔5-10秒通过对比观察线程状态的变化。jmap -heap pid查看堆内存概要信息。jmap -histo:live pid查看堆内存中对象的统计信息快速定位疑似内存泄漏的大对象。jstat -gcutil pid 1000 10每1秒采样一次GC情况共10次用于观察GC频率和耗时。系统级命令netstat -antp | grep pid或ss -antp | grep pid查看该进程持有的所有网络连接状态。重点关注CLOSE_WAIT、TIME_WAIT数量是否异常增多。top -Hp pid查看该进程内各个线程的CPU和内存占用情况。可以结合jstack的线程IDnid通常是十六进制来定位消耗资源的线程。vmstat 1或sar查看系统整体的CPU、内存、IO等待情况。日志分析这是“问诊”的关键。集中查看假死时间点前后通常前后5-10分钟的日志Tomcat日志catalina.out,localhost.log,localhost_access_log.应用日志你的Spring Boot、Spring MVC或其他框架的日志文件。GC日志如果配置了JVM参数-Xloggc这里会有最详细的垃圾回收记录。注意在生产环境执行这些命令尤其是jmap和频繁的jstack本身会带来一定的性能开销Stop-The-World可能会加剧问题或影响正常请求。务必在业务低峰期操作或通过监控系统预设的报警触发自动抓取。3. 核心排查路径一线程死锁与资源竞争拿到线程转储Thread Dump后我们首先要排查的就是经典的死锁问题。用文本编辑器打开jstack输出的文件直接搜索“deadlock”或“Found one Java-level deadlock:”。如果幸运或者说是不幸地找到了jstack通常会清晰地指出哪些线程在互相等待哪些锁。但更多时候假死并非严格的死锁而是线程池耗尽或资源竞争导致的全局性阻塞。这时你需要分析线程的“状态”State。重点关注以下几种状态BLOCKED (on object monitor)线程在等待进入一个同步方法或代码块。如果大量业务线程比如http-nio-8080-exec-*都阻塞在同一个锁对象上说明存在热点锁竞争。这可能是因为不恰当地在方法上使用了synchronized或者错误地使用了一个全局锁例如一个静态的HashMap进行频繁的读写。WAITING (parking)线程在等待某个条件如Object.wait()。这常见于任务队列已满工作线程无事可做。RUNNABLE线程正在执行。但如果一个线程长时间处于RUNNABLE状态且一直持有锁也可能导致其他线程阻塞。需要看它正在执行什么代码。实操分析示例 假设你的线程转储里有30个http-nio-8080-exec-5这样的线程状态都是BLOCKED并且都在等待锁0x00000000f4a1c0d8。通过查找哪个线程持有这个锁jstack会显示locked 0x00000000f4a1c0d8你发现是一个名为AsyncProcessorThread的线程持有。进一步看这个线程的栈发现它卡在了一个数据库查询或者一个缓慢的外部HTTP调用上。根源就找到了一个慢操作持有了公共锁阻塞了所有Web请求线程。关于HashMap的潜在风险在排查时如果看到大量线程栈中涉及HashMap的put或get操作尤其是在并发环境下使用未同步的HashMap这本身就是一个风险点。虽然HashMap的线程不安全通常表现为数据错乱而非直接死锁但在Java 7及之前版本高并发put导致扩容时可能形成循环链表引发CPU飙升。更常见的是开发者用一个静态的HashMap作为缓存却没有做好并发控制如使用ConcurrentHashMap或加锁导致多个线程修改时内部状态不一致也可能间接引发问题。4. 核心排查路径二内存泄漏与GC风暴内存问题导致的假死非常隐蔽。现象可能是服务响应越来越慢最后停滞。此时jstat是你的第一道探测器。运行jstat -gcutil pid 1000观察关键指标O(Old Generation利用率)如果持续保持在95%以上甚至100%说明老年代快满了。FGC/FGCT(Full GC次数/耗时)如果FGC在短时间内疯狂上涨FGCT耗时很长例如每次都要数秒说明系统正在经历“GC风暴”。Full GC会暂停所有应用线程Stop-The-World如果频繁发生且耗时久从外部看就是服务间歇性或持续性地无响应。下一步用jmap揪出元凶jmap -histo:live pid | head -50查看存活对象中数量最多、占用空间最大的类。常见嫌疑犯是自定义的类、char[]字符串、byte[]网络传输、文件操作以及一些框架内部对象。如果怀疑是内存泄漏可以生成堆转储文件进行深度分析jmap -dump:live,formatb,fileheap.hprof pid。然后用MATMemory Analyzer Tool或JVisualVM打开这个.hprof文件。MAT的“Leak Suspects Report”功能非常强大能自动分析出可能泄漏的对象引用链。典型的内存泄漏场景静态集合类滥用例如在HashMap或List中缓存了用户会话对象、查询结果集并且只添加不清理。未关闭的资源数据库连接、文件流、网络连接HttpClient未在finally块中关闭。线程局部变量(ThreadLocal)使用不当特别是在使用线程池Tomcat的请求处理就是线程池时如果ThreadLocal中存放大对象且用完后未调用remove()则该对象会在线程存活期间一直存在因为线程池的线程是会复用的。第三方库或框架的Bug某些旧版本的框架或连接池可能存在已知的内存泄漏问题。5. 核心排查路径三网络连接与IO问题当应用大量依赖外部服务数据库、缓存、微服务时网络问题会直接传导至应用层造成假死。这里的关键命令是netstat或ss。执行netstat -antp | grep tomcat_pid仔细查看连接状态海量的CLOSE_WAIT状态连接这是最经典的信号之一。CLOSE_WAIT表示对方客户端已经关闭了连接发送了FIN但我方服务端的应用代码没有正确地关闭套接字。如果CLOSE_WAIT连接数持续增长会快速耗尽系统的可用端口和文件描述符导致新的连接无法建立。根本原因通常是应用没有在finally块中关闭网络资源如数据库连接、HTTP连接。大量的TIME_WAIT连接这在高并发短连接场景下比较常见。如果数量过多数万可能会占满本地端口。可以调整系统内核参数如net.ipv4.tcp_tw_reuse和net.ipv4.tcp_tw_recycle但需谨慎来缓解但更优解是优化应用使用连接池如数据库连接池DBCP/HikariCPHTTP连接池来复用长连接。IO阻塞导致的线程停顿 除了网络IO文件IO也可能成为瓶颈。如果线程转储显示大量线程处于RUNNABLE状态但栈顶是java.io.FileInputStream.read或类似的Native方法并且持续很久说明可能正在读写一个巨大的文件或者磁盘IO性能极差使用iostat命令验证。这会导致处理该请求的线程被长时间占用如果并发请求都涉及IO线程池很快就会被耗尽的等待IO的线程占满。6. 实战排查流程与问题复现模拟让我们模拟一个完整的排查流程。假设监控报警服务无响应但进程存活。第一步快速状态确认ssh登录服务器ps -ef | grep tomcat获取PID。curl -m 5 http://localhost:8080/health测试应用确认超时。tail -100f logs/catalina.out查看最新日志有无异常堆栈如OutOfMemoryError。第二步抓取即时快照jstack -l PID jstack_$(date %s).log抓取第一次线程转储。jstat -gcutil PID 1000 5观察GC情况。netstat -antp | grep PID | wc -l以及netstat -ant | grep CLOSE_WAIT | wc -l统计连接数。第三步分析并定位可能方向如果jstat显示FGC频繁且Old区满重点怀疑内存泄漏立即用jmap -histo查看对象。如果netstat显示CLOSE_WAIT上千重点怀疑连接未关闭去代码中检查所有网络操作。如果以上都正常重点分析线程转储。将jstack日志文件上传到在线分析工具如fastthread.io或使用本地的jstack分析脚本查看线程状态分布。发现80%的线程是BLOCKED状态立刻查找他们等待的锁和持有锁的线程在做什么。第四步根因验证与修复找到可疑点后需要结合代码和日志进行验证。例如怀疑是某个查询慢导致锁竞争就去数据库慢查询日志中对应时间点查找怀疑是内存泄漏就分析jmap输出的顶级对象所属的类在代码中查找该类的引用点。实操心得很多时候问题不是单一的。可能是内存缓慢泄漏导致Full GC越来越频繁每次GC停顿时间变长使得部分请求超时超时的请求客户端断开产生CLOSE_WAIT同时GC停顿又使得线程处理变慢任务队列堆积最终全面崩溃。因此需要综合多项指标判断。7. 常见问题速查与预防措施根据经验我整理了一份Tomcat假死常见原因速查表你可以像查字典一样对照症状找可能原因现象/监控指标可能原因排查工具与重点CPU使用率正常但请求无响应1.线程死锁2.全局锁竞争3.外部依赖DB、API超时jstack看线程状态和锁信息检查外部服务监控。CPU使用率100%1.无限循环/递归2.频繁的Young GC3.HashMap并发扩容Java 7top -Hp找高CPU线程jstack对应nid看栈jstat看GC。内存使用率持续增长FGC频繁内存泄漏jmap -histo,jmap -dumpMAT分析堆转储。CLOSE_WAIT连接数异常高网络连接未正确关闭Socket, DB Connection, HttpClientnetstat代码审查finally块中的资源关闭。磁盘IO等待高%util日志打印过频、同步写文件、磁盘慢iostat -x 1检查日志配置如Logback的immediateFlush。线程池活跃线程数达最大值1.任务处理过慢慢SQL、慢逻辑 2.任务队列满调整maxThreads、acceptCount优化业务逻辑。预防胜于治疗一些有效的预防措施包括代码层面避免使用synchronized修饰整个方法或使用粗粒度锁。优先考虑并发容器ConcurrentHashMap、显式锁ReentrantLock或无锁设计。对静态集合类如用作缓存的HashMap的访问必须做好同步或直接使用ConcurrentHashMap。所有InputStream、OutputStream、Connection、HttpClient等资源必须在try-with-resources或finally块中确保关闭。谨慎使用ThreadLocal用完后务必调用remove()。配置层面为JVM配置合理的堆大小-Xms,-Xmx和GC参数并务必开启GC日志-Xloggc:... -XX:PrintGCDetails -XX:PrintGCDateStamps。配置Tomcat的连接器Connector参数如maxThreads处理请求的最大线程数、acceptCount等待队列长度使其与你的硬件和业务负载匹配。使用Druid、HikariCP等成熟的连接池并配置合理的超时时间连接超时、查询超时、空闲检测。监控层面建立完善的应用监控JVM内存、GC次数与时间、线程池状态、关键接口响应时间与QPS。设置关键指标报警如Full GC频率、线程池活跃度、CLOSE_WAIT数量、接口超时率等。8. 高级工具与持续 profiling对于更复杂、间歇性出现的问题上述一次性快照可能不够。这时需要引入持续性的Profiling工具记录一段时间内的应用行为。Arthas阿里开源的Java诊断神器堪称线上排查的瑞士军刀。它可以在不重启应用的情况下动态跟踪方法调用耗时、查看方法入参返回值、监控线程状态、甚至热修改代码。例如使用trace命令追踪某个慢方法的调用路径和耗时使用thread命令查看所有线程的繁忙状态比反复执行jstack更方便。APM工具如SkyWalking、Pinpoint。它们通过字节码增强技术自动追踪每一次请求的完整调用链包括跨服务的调用。当发生假死或慢请求时你可以清晰地看到时间消耗在哪个服务、哪个数据库语句、甚至哪一行代码上。这对于微服务架构下的问题定位是革命性的。JMX与VisualVM对于测试或预发环境可以通过JMX远程连接使用VisualVM进行实时的可视化监控包括CPU、内存、线程的图表以及抽样器Sampler来定位CPU热点或内存分配热点。排查Tomcat假死问题是一个综合运用操作系统、网络、JVM和应用知识的过程。它没有一成不变的答案但遵循“由外而内、先整体后局部、抓取快照对比分析”的思路结合强大的工具绝大多数问题都能被定位。最后记住每一次线上问题的解决都是对系统脆弱点的一次认知升级把排查过程中发现的问题根因转化为代码规范、配置检查清单和监控报警项才能让系统越发稳健。
返回列表