JVM假死不是进程崩溃,而是线程全部停摆或GC完全失控,排查核心路径是先抓Thread Dump和GC日志,再对症处理,多数情况下问题出在Full GC频繁、死锁或外部资源阻塞上。
JVM假死怎么判断:先确认是真死还是假死
很多团队遇到过这种场景:Java服务进程还在,端口也通,但请求就是没响应,CPU占用率忽高忽低,日志停留在某个时间点不再更新,这时候先别急着重启,重启确实能救急,但找不到根因,下次还会翻车。
第一步,确认进程还活着,用jps -l看进程ID还在不在,再用jstack <pid>尝试打印线程快照,如果jstack卡住或者报错,说明JVM内部已经非常不健康,如果jstack能正常输出,恭喜你,还有救,拿到现场的线索了。
第二步,区分是哪种假死形态。
- 完全无响应:请求不进,日志不推,有点像被点了穴。
- 间歇性卡顿:一段时间能用,一段时间完全阻塞,心跳正常但业务超时。
- 吞吐量骤降:服务没挂,但请求RT飙升到几十秒,看起来跟挂了差不多。
这几种形态的排查方向不太一样,后面逐个拆解。
jvm假死怎么排查:四步定位法
排查JVM假死不能靠猜,得按顺序走,每一步都能缩小范围,以下排查思路是行业共识,按这套流程走能覆盖绝大多数假死场景。
第一步:抓Thread Dump,看线程在干什么
Thread Dump是排查JVM假死的第一现场,用jstack <pid> > dump.txt把线程快照存下来,连续抓三次,每次间隔几秒,对比线程状态的变化。
重点看这几个状态:
- BLOCKED:线程在等待锁,如果大量线程卡在同一个锁上,基本就是死锁或者锁竞争失控。
- WAITING / TIMED_WAITING:线程在等条件变量或者睡大觉,数量多是正常的,但要看它们卡在哪个类上。
- RUNNABLE:这个状态有时候是陷阱,Java线程处于RUNNABLE不代表真的在跑,可能卡在JVM内部或者Native方法上。
如果多次抓取的dump里,主业务线程一直停留在同一个方法的同一行代码上,基本可以断定就是这里的阻塞,攒三个dump放在一起对比,如果某个线程状态始终不变,那就是它的问题了。
第二步:看GC日志,排除垃圾回收瓶颈
GC导致的假死是JVM假死场景中占比最大的一个原因,经常表现为请求堆积、CPU忽高忽低、服务间歇性停摆,用jstat -gcutil <pid> 1000观察实时GC情况,重点看Full GC的发生频率和耗时。
典型的GC假死特征:
- Full GC频繁触发,每次耗时几十秒甚至几分钟
- Old区占用率居高不下,回收完立刻又涨上去
jstat输出显示FGC次数持续增长,且FGCT占比极高- JVM发生“GC时的STW(Stop The World)”,所有应用线程全部暂停,对外看起来就是假死
采集GC日志的方式,在JVM启动参数里加上-Xloggc:/data/logs/gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps,下次出问题时直接翻日志,看每次GC的时间点和耗时。
第三步:查外部资源状态,别让JVM背锅
有时JVM线程卡住不是自己的问题,而是waiting在外部依赖上,比如数据库连接池耗尽、Redis超时、下游接口响应慢,都会让大量线程堵在等待资源的地方。
用jstack配合网络命令排查:
- 抓一份dump,看有多少线程卡在
java.net.SocketInputStream.socketRead0这种Native方法上 - 用
netstat -anp | grep <pid>看这个进程建立了多少TCP连接,连接数是否异常 - 检查连接池的使用率,比如Druid的活跃连接数、Tomcat的线程池使用情况
这里有一个经常被忽略的场景:数据库连接池被打满,当连接池里的连接全部被占用且没有及时释放时,所有获取连接的线程都会阻塞,这时候jstack里能看到大量线程卡在getConnection相关的方法上。
第四步:结合日志和监控,确认假死的时间线
把Thread Dump的线程状态、GC日志的STW时间、业务日志的最后输出时间,这三者放在时间轴上对齐,比如业务日志停在14:32:05,而GC日志显示14:32:07开始了一次长达30秒的Full GC,那问题就很明显了。
线上jvm卡死解决方案:分场景对症处理
定位到原因之后,解决方案就有针对性了。
Full GC频繁导致假死
Full GC频繁的根源在于对象分配太快、老年代空间不足或者存在大对象,先确认是不是内存泄漏,用jmap -histo <pid> | head -50查看堆内存中对象实例排名,如果某个业务对象数量异常多,基本可以锁定内存泄漏点。
解决步骤:
- 优化代码,减少不必要的对象创建,特别是循环内部的大对象
- 调整堆内存参数,比如
-Xmx设置过小就适当扩大,但不要超过物理内存的70% - 选择合适的垃圾收集器,JDK 11+优先用G1,大堆内存场景可以考虑ZGC
- 排查是否有大缓存或者静态集合无限增长,这是老年代撑满的常见原因
预防性检查清单:
- 查看老年代使用率曲线,如果呈持续上升趋势,说明内存存在累积
- 定期执行
jmap -dump分析堆转储文件,确认对象存活情况 - 关注GC日志中晋升失败(Promotion Failed)和并发模式失败(Concurrent Mode Failure)记录,这两类报错往往意味着堆配置不合理
死锁导致线程集体停摆
多线程业务一旦发生死锁,表现就是所有相关线程卡在BLOCKED状态,谁也动不了,用jstack查看到底谁持有了锁、谁在等锁。
快速验证:在jstack输出的最后,JVM会主动检测并打印死锁信息,看到”Found one Java-level deadlock”这样的字样,就实锤了。
处置方式:
- 代码层面避免多把锁嵌套使用,尽量用
tryLock代替synchronized,加超时时间 - 对锁的获取顺序做统一约定,比如总是先拿A锁再拿B锁,从根上消除循环等待
- 死锁发生后,重启可以暂时恢复,但要复现并修复代码逻辑,否则下次一样触发
外部依赖阻塞导致线程池耗尽
当线程池所有线程都在等待数据库/Redis/远程接口响应时,新的请求无处安放,服务表现就是假死,这种问题用jstack能看到大量线程处于WAITING状态,且都指向同一个外部资源调用。
解决思路:
- 给所有外部调用设置超时时间,忌用无超时的socket连接
- 使用线程池隔离,比如把数据库调用、HTTP调用、MQ消费放到不同的线程池,单一依赖出问题不拖垮整个应用
- 引入熔断降级机制,下游故障时快速失败而不是傻等,这是避免线程池被拖垮的有效手段
- 巡检连接池配置,确认最大连接数、最小空闲数、获取连接超时时间这些参数是合理的
内存溢出引发JVM濒死
内存溢出发生时,JVM可能频繁尝试GC但回收不到空间,整个进程处于垂死挣扎状态,用jmap -dump:format=b,file=heap.bin <pid>导出堆快照,再用MAT分析大对象和引用链,如果无法导出堆快照,jmap -histo:live也能做些粗略判断,但准确性不如完整的dump分析。
应对策略:
- 如果堆爆了而且一时半会定位不了,先重启保服务,然后保留现场分析
- 分析dump中的内存占用排名,重点看缓存、集合类、线程栈相关的内存
- 修复后要加内存监控和dump自动导出机制,下次出问题能自动保留现场
关于jvm full gc频繁怎么排查的一个高频细节
很多人在排查Full GC问题时忽略了一个细节:让Full GC频繁发生的可能不是应用代码,而是Metaspace区或直接内存(Direct Memory)耗尽,Metaspace默认是动态扩容的,当加载的类太多时会触发Full GC;而Direct Memory虽然不占堆空间,但它归GC管理,耗尽时会抛OOM且表现非常诡异。
排查时除了看堆内存,也要看jstat -gcutil里M区(Metaspace)的使用率,以及用jcmd <pid> VM.native_memory查看Native内存的占用情况,尤其是用Netty等框架做网络编程时,Direct Memory问题会更常见。
JVM假死的最后一道防线:监控和告警
排查做得再好,也不如防范于未然,搭一套监控体系,让假死苗头刚出现就被捕获,比事后找原因效率高得多。
监控项推荐:
- GC频率和耗时:Full GC超过3秒就要告警,超过10秒基本已经造成业务影响
- 线程状态分布:BLOCKED线程数超过阈值触发告警
- 连接池使用率:数据库连接池和HTTP连接池的使用率超过80%预警
- JVM CPU占用:注意是JVM进程的CPU占用,不是整机CPU占用
工具有很多选择:Arthas、Prometheus + Grafana、SkyWalking都能做JVM监控,其中Arthas特别值得推荐,属于线上诊断利器,不用重启进程就能做各种动态排查,它有一个dashboard命令,一行就能看到整体线程状态和内存分布,这在排查JVM假死时非常好用。
一个务实的建议:出问题先别急着在群里找原因,带上有用的信息再说话,一次完整的故障信息应该包含:jstack输出、GC日志、堆内存使用率、最近一段时间的变更日志、网络和负载情况,把这些材料备齐再跟同事一起看,效率最高。
业内专家指出:JVM假死的本质是资源争用或者停顿问题,排查工作的核心是快速确认停摆的线程在等什么资源,谁占用了这个资源,以及为什么没有释放,围绕这三个问题去找答案,就不会被表面现象绕晕。
JVM假死是线上环境最棘手的问题之一,但它的排查路径其实很固定:抓线程快照确认线程状态,同步查看GC日志和外部依赖,再结合堆内存分析定位根因,掌握了jstack、jstat、jmap、jcmd这几个工具的使用,配合合理的监控告警,JVM假死的应对就能从容很多,下次线上再碰到服务卡住,优先保存现场再动手,别急着重启。
关于jvm虚拟机假死的常见问答
jvm假死和jvm崩溃有什么区别?
假死是进程还在、线程卡住或GC停摆,可以通过jstack和jstat诊断;崩溃是进程直接消失,比如OutOfMemoryError导致进程退出或者JVM自身crash,后者需要看hs_err_pid错误日志和系统dmesg(这一指标在Java官方故障排查指南中有明确说明),假死通常有恢复的空间,崩溃则需要靠重启和高可用机制来兜底。
为什么java服务假死时cpu占用低?
这种情况多半是线程在等待锁、等待IO或者处于sleep状态,而不是空转,CPU占用低恰恰说明线程不是在计算,而是在”干等”,抓一份jstack看看线程卡在哪个方法上,如果是socketRead就是等网络,如果是LockSupport.park就是等锁,有时候也可能是JVM进入GC后线程全部暂停导致CPU空置。
jvm full gc频繁怎么排查最有效?
两个步骤走:先看GC日志确认Full GC触发的原因,是空间分配担保失败还是Metaspace不足,可以用-XX:+PrintGCDetails参数记录细节;再用jmap -histo或jmap -dump分析堆内对象分布,找出谁占了堆空间,大部分Full GC频繁都是内存泄漏或者堆配置不当导致的,修完代码还不够,要调优堆参数并加监控,防止复发。
首发原创文章,作者:王坚,如若转载,请注明出处:https://idctop.com/article/772112.html


