本文已收录于《JVM生产环境问题定位与解决实战》专栏,完整系列见文末目录
引言
在前五篇文章中,我们深入探讨了JVM生产环境问题定位与解决的实战技巧,从基础的jps、jmap、jstat、jstack、jcmd等工具,到JConsole、VisualVM、MAT的高级应用,再到Java飞行记录器(JFR)的强大功能,以及如何使用JMC进行JFR性能分析,最后介绍了Arthas这一不可错过的故障诊断利器。本篇文章作为总结篇,将梳理在面对JVM各种问题时我们的定位思路,并给出工具选择的策略。
一、JVM问题分类与核心场景
在定位问题前,需先明确问题类型。以下是JVM常见问题分类及典型特征:
1. 内存相关问题
- 特征:内存泄漏、堆内存溢出(OOM)、元空间溢出、内存使用异常波动
- 典型表现:
java.lang.OutOfMemoryError
、JVM进程内存持续增长、GC频率异常
2. 性能瓶颈问题
- 特征:响应延迟、吞吐量下降、CPU使用率异常
- 典型表现:接口响应时间突增、TPS骤降、CPU 100%占用
3. 线程相关问题
- 特征:线程阻塞、死锁、线程池异常、线程泄漏
- 典型表现:线程数异常增长、接口无响应、
java.util.concurrent
包异常
4. GC相关问题
- 特征:GC停顿时间过长、Full GC频繁、GC日志异常
- 典型表现:JVM日志频繁出现Full GC、业务线程长时间被阻塞
5. 其他问题
- 类加载异常、本地内存泄漏(Native Memory)、JVM崩溃(core dump)
二. JVM问题定位的一般思路
在生产环境中,JVM可能会遇到各种问题,比如CPU使用率过高、内存泄漏、线程死锁、性能瓶颈等。定位和解决这些问题的过程通常可以分为以下几个步骤:
- 监控与发现:通过操作系统提供的资源监控工具(如top、htop、free)、arthas、JVM自带工具(jps、jstat)或者搭建的监控系统(如Prometheus、Grafana)发现异常指标,如CPU飙升、内存溢出、系统宕机、gc异常等。
- 评估是否需要重启服务:根据问题的严重性和业务影响,决定是否立即重启服务以恢复业务。
- 快照捕获:使用合适的工具收集数据(如线程堆栈、堆转储文件、GC日志、应用日志等)。
- 数据分析:根据异常类型选择工具分析结果,确定问题的根源,例如代码缺陷、配置不当或资源竞争。
- 解决问题:采取针对性措施,如调整JVM参数、优化代码或修复逻辑错误。
- 验证与监控:解决问题后,持续监控系统,确保问题不再复现。
在这一过程中,选择合适的工具是关键。不同的工具适用于不同的场景和问题类型,接下来我们将对这些工具进行分类,并探讨如何根据具体问题选择合适的工具。
三. JVM工具分类
根据工具的使用方式和功能,我们可以将前五篇文章中介绍的工具分为以下几类:
-
命令行工具 :
- jps:查看JVM进程。
- jmap:分析堆内存使用情况。
- jstat:监控GC和JVM运行时统计信息。
- jstack:查看线程堆栈。
- jcmd:多功能命令行工具。
- Arthas:在线诊断工具,支持线程分析、反编译等。
-
图形化工具:
- JConsole:实时监控JVM运行状态。
- VisualVM:多功能的JVM监控和分析工具。
- MAT(Memory Analyzer Tool):深入分析堆内存转储。
- JMC(Java Mission Control):分析JFR记录的性能数据。
-
性能分析工具:
- JFR(Java Flight Recorder):内置于JDK的事件记录工具。
- JMC:与JFR配合使用,分析性能瓶颈。
命令行工具适合在服务器上快速诊断,图形化工具适合本地深入分析,而性能分析工具则专注于性能调优。
四. 常见JVM问题及排查思路
以下是生产环境中常见的JVM问题,并针对每个问题提供可能的原因推测和实用的排查思路。
1 JVM进程突然消失
可能原因
- OOM Killer:操作系统因内存不足触发Out-Of-Memory Killer,杀死了JVM进程。
- JVM崩溃:JNI调用、本地代码bug或JVM本身的bug导致崩溃(会生成hs_err_pid.log文件)。
- 外部干预:人为操作(如kill -9)或脚本误杀JVM进程。
排查思路
- 检查JVM日志:查看
hs_err_pid<pid>.log
文件(通常在工作目录下生成),分析崩溃堆栈。 - 检查系统日志:查看
/var/log/messages
或dmesg
(grep -i 'oom' /var/log/messages*
、grep -i 'sigterm' /var/log/syslog
),确认是否被OOM Killer终止。 - 确认外部操作:检查服务器操作日志或运维记录,排除人为误操作。
- 启用JVM参数:添加
-XX:+HeapDumpOnOutOfMemoryError
生成堆转储文件;
2 内存使用率过高
可能原因
- 内存泄漏:对象未被正确释放,堆内存持续增长。
- 不合理的堆配置:
-Xmx
和-Xms
设置过小或过大。 - Metaspace溢出:类加载过多或动态代理使用不当。
- 本地内存泄漏:JNI或NIO的Direct ByteBuffer未释放。
排查思路
- 监控内存使用
- 使用
jstat -gc
查看堆内存和Metaspace使用情况。 - 用Arthas的
dashboard
命令实时观察内存状态。 - 通过JMC连接JVM,查看“Memory”视图中的堆使用趋势。
- 使用
- 生成堆转储并分析内存泄漏
- 用
jmap -dump:live,format=b,file=heap.hprof <pid>
或Arthas的heapdump /tmp/heap.hprof
导出堆快照,结合MAT分析,通过Leak Suspects
视图查看泄露疑点。或者选中 Retained Heap 最大的对象,右键选择"List objects > with incoming references"
,查看有哪些对象引用了它。通过"Path to GC Roots"
(到 GC 根的路径)功能,找到该对象为什么没有被垃圾回收。选择"excluding weak/soft references"(
排除弱引用/软引用),以聚焦强引用路径。结合 MAT 的类名和引用链,推测可能的代码路径。 - 用JFR记录(
-XX:StartFlightRecording,settings=profile
),在JMC的“Allocation”视图识别内存分配热点和泄漏对象。
- 用
- 检查GC日志
- 开启JVM参数
-XX:+PrintGCDetails
和-Xlog:gc*
,分析GC频率和效果。 - 结合Arthas的
dashboard
或JMC的“Garbage Collection”视图验证GC行为。
- 开启JVM参数
- 检查代码和类加载
- 用Arthas的
sc -d <className>
检查类加载详情,排查Metaspace问题。 - 用JFR的“Class Loading”事件和JMC分析类加载行为。
- 用JMC的“Object Reference”视图追踪未释放对象的引用链,定位内存泄漏根源。
- 用Arthas的
典型操作
jmap -dump:format=b,file=heap.hprof <pid> # 生成堆转储
jstat -gcutil <pid> 1000 # 每秒实时GC统计
jcmd <pid> VM.native_memory # 堆外内存分析(需开启NMT)
3 CPU使用率过高
可能原因
- 死循环或高计算任务:代码中存在无限循环或复杂计算逻辑。
- 线程竞争激烈:锁争用或线程池配置不当。
- 频繁Full GC:垃圾回收过于频繁,占用CPU资源。
排查思路
- 定位高CPU线程
- 使用
top -H -p <pid>
查看线程CPU占用,记录线程ID(转换为16进制)。 - 用Arthas的
thread
命令列出线程并找到CPU占用最高的线程。 - 用JFR(
-XX:StartFlightRecording
)在JMC的“CPU Load”或“Thread”视图中定位高CPU线程。
- 使用
- 获取线程栈
- 通过
jstack <pid>
生成线程调用栈,定位死循环或阻塞点。 - 用Arthas的
thread <threadId>
查看具体线程堆栈。 - 用JFR的“Method Profiling”在JMC中分析线程执行热点。
- 通过
- 分析代码逻辑
- 检查是否存在死循环、频繁GC或高负载任务。
- 检查GC行为
- 开启GC日志(
-XX:+PrintGCDetails
),分析Full GC频率。 - 结合MAT分析,Histogram视图中找到 Retained Heap 最大的对象,右键选择
"List objects > with incoming references"
,查看有哪些对象引用了它。通过"Path to GC Roots"
(到 GC 根的路径)功能,找到该对象为什么没有被垃圾回收。选择"excluding weak/soft references"
(排除弱引用/软引用),以聚焦强引用路径。结合 MAT 的类名和引用链,推测可能的代码路径。 - 用Arthas的
vmtool --action getInstances --className java.lang.Object
检查对象创建情况。 - 用JMC的“Garbage Collection”视图确认GC对CPU的影响。
- 开启GC日志(
典型操作
top -H -p <pid> # 查看线程CPU占用,记录下占用CPU最高的线程TID。
printf "%x\n" <TID> #将TID转换为十六进制(Java堆栈中的线程ID是以十六进制表示的)
jstack <pid> > thread_dump.txt # 抓取线程栈,在生成的thread_dump.txt中搜索对应的十六进制线程ID,找到该线程的堆栈信息。
---------------------------------------------------------------
thread -n 3 # 显示最忙的3个线程(启动Arthas后执行)
4 线程死锁
可能原因
- 锁顺序不当:多个线程以不同顺序获取多个锁。
- 资源竞争:线程间对共享资源访问未正确同步。
- 第三方库问题:依赖库中的锁使用不当。
排查思路
- 检测死锁
jstack <pid>
输出末尾会明确提示Found one Java-level deadlock
- 用Arthas的
thread -b
直接检测并输出死锁信息。 - 用JFR记录(
-XX:StartFlightRecording
),在JMC的“Lock Instances”视图中检测锁竞争和死锁。 - 使用JConsole或VisualVM的线程监控功能查看线程状态
- 分析死锁详情
jstack
提示“Found one Java-level deadlock”,定位锁冲突点。- 用Arthas的
thread <threadId>
查看具体线程堆栈。 - 用JMC的“Deadlock Detection”功能、JConsole或者VisualVM的线程视图展示锁冲突点和线程状态。
- 检查代码
- 梳理加锁逻辑,确保锁获取顺序一致。
- 动态监控锁竞争
- 用Arthas的
monitor
命令监控关键方法的执行频率和锁等待时间。 - 用JMC的“Thread”视图分析锁等待时长和竞争热点,结合“Contended Locks”事件优化同步代码。
- 用Arthas的
5 性能瓶颈
可能原因
- I/O阻塞:数据库查询慢或文件操作耗时。
- 锁等待:同步块或线程池任务排队。
- GC暂停:STW(Stop-The-World)时间过长。
- 网络延迟:外部服务响应慢。
排查思路
- 性能分析
- 使用
jvisualvm
采样,定位耗时方法。 - 用Arthas的
trace <className> <methodName>
追踪方法执行时间。 - 用JFR(
-XX:StartFlightRecording,settings=profile
)记录,在JMC的“Method Profiling”视图定位耗时方法,结合“Latency”视图分析瓶颈来源。
- 使用
- 检查线程状态
- 通过
jstack
分析线程是否在WAITING
或TIMED_WAITING
状态。 - 用Arthas的
thread -state WAITING
筛选等待线程。 - 用JMC的“Thread”视图查看线程等待和阻塞详情。
- 通过
- 优化GC
- 调整
-XX:+UseG1GC
或 WiresharkCMS参数,减少暂停时间。 - 用Arthas的
dashboard
监控GC状态。 - 用JMC的“Garbage Collection”视图分析GC暂停时间。
- 调整
- 外部依赖排查
- 使用日志或Wireshark抓包分析网络耗时。
- 用Arthas的
watch <className> <methodName>
观察方法入参和返回值。 - 用JFR的“I/O”事件和JMC分析文件或网络操作耗时。
典型操作
# 使用Arthas的trace命令追踪方法执行时间。
trace com.ExampleController getUserInfo "#cost > 200"
# 输出结果
`---[213.3576ms] com.ExampleController:getUserInfo()
+---[212.12ms] com.UserService:queryFromDB() # 发现DB查询耗时
| `---[210.45ms] org.mybatis.spring.SqlSessionTemplate:selectOne()
`---[1.2ms] com.Utils:maskPhoneNumber()
watch com.example.Service::handleRequest '{args,requestTime}'
6 GC问题
典型表现
- 频繁Full GC:
jstat -gcutil
中FGC列快速增加 - GC停顿过长:通过
-Xlog:gc*
日志观察暂停时间 - 晋升失败:年轻代对象过快进入老年代
可能原因
- GC频率过高:对象创建速度快,年轻代空间不足。
- Full GC频繁:老年代填满,触发长时间STW。
- GC配置不当:垃圾回收器选择或参数设置不合理。
排查思路
- 开启GC日志并实时监控
- 配置
-XX:+PrintGCDetails -Xlog:gc*
,分析GC耗时和效果。 - 用Arthas的
dashboard
实时查看GC状态。 - 用JMC的“Garbage Collection”视图分析GC次数和暂停时间。
- 配置
- 监控内存分区
- 用
jstat -gcutil <pid>
观察Eden、Survivor、Old区使用率。 - 用Arthas的
ognl '@java.lang.management.ManagementFactory@getGarbageCollectorMXBeans()'
获取GC统计。 - 用JFR的“Memory”视图和JMC分析内存分配与GC行为。
- 用
- 调整堆大小并分析GC根因
- 根据业务负载调整
-Xmx
、-Xms
和-XX:NewRatio
。 - 用Arthas的
heapdump
生成堆快照分析老年代对象。 - 用JFR(
-XX:StartFlightRecording,dump-on-exit=true
)生成堆快照,在JMC的“Allocation”视图定位高频分配对象,优化代码减少GC压力。
- 根据业务负载调整
- 选择合适GC算法
- 低延迟场景用G1,高吞吐量用Parallel GC。
- 结合JMC的“GC Times”和“GC Pause”分析优化参数。
- 调整堆大小
-Xms
和-Xmx
设为相同值,避免动态扩容
五. 先重启还是先排查的选择
在生产环境中遇到JVM问题时,开发者往往面临一个抉择:是立即重启服务以快速恢复,还是先排查问题以避免复发?以下是选择依据及后续排查措施。
判断依据
- 问题严重性:
- 如果服务完全不可用(如JVM进程消失或响应超时),且影响核心业务,优先考虑重启以恢复服务。
- 如果问题仅表现为性能下降或间歇性异常,可先尝试在线排查。
- 数据丢失风险:
- 重启可能导致内存中未持久化的数据丢失,需评估业务影响。
- 复现难度:
- 如果问题难以复现,重启前尽量收集现场数据(如堆转储、线程栈、JFR记录)。
必须立即恢复服务的措施
若必须马上恢复服务,可采取以下措施在不中断排查的情况下继续分析问题:
- 收集关键数据后重启
- 在重启前快速执行
jmap -dump:live,format=b,file=heap.hprof <pid>
生成堆转储。 - 用
jstack <pid>
抓取线程栈。 - 确保JFR已启用(
-XX:StartFlightRecording,dumponexit=true
),保存退出时的记录文件。
- 在重启前快速执行
- 流量复制到测试环境
- 配置流量镜像工具(如TCPdump或服务网格的Sidecar),将生产流量复制到测试环境。
- 在测试环境回放流量,观察是否复现问题。
- 在测试环境复现
- 根据生产环境的日志、请求参数和负载特征,构造测试用例。
- 在测试环境启用JFR(
-XX:StartFlightRecording
)和Arthas,模拟问题场景。
- 开启新节点并隔离问题节点
- 部署新JVM节点接管流量,保持问题节点运行但不接收新请求。
- 在隔离节点上用Arthas(
dashboard
、thread
)或JMC实时分析,避免影响服务。
六. 工具选择指南
在实际工作中,面对JVM问题时,可以参考以下指南选择工具:
- 快速诊断:
- 在服务器上使用命令行工具(如jstack、jmap、Arthas)快速获取初步信息。
- 示例:CPU过高时,运行jstack > stack.txt查看线程堆栈。
- 深入分析:
- 将堆转储文件或JFR记录下载到本地,使用MAT、JMC等工具进行详细分析。
- 示例:分析内存泄漏时,用MAT打开jmap生成的heap.hprof文件。
- 实时监控:
- 使用JConsole或VisualVM观察JVM的实时状态,适合初步排查。
- 示例:连接到远程JVM,监控内存和线程变化。
- 性能分析:
- 使用JFR和JMC进行全面性能分析,适合长期优化。
- 示例:运行JFR记录1分钟数据,用JMC查看方法耗时分布。
- 在线诊断:
- 在不重启JVM的情况下,使用Arthas进行动态排查。
- 示例:怀疑某方法有问题,用jad com.example.MyClass反编译代码。
JVM问题的排查需要综合运用工具、日志和代码分析。传统工具如jps
(查看进程)、jmap
(堆转储)、jstack
(线程转储)、jstat
(GC监控),结合Arthas的动态诊断和JMC/JFR的深度分析能力,可以覆盖内存泄漏、锁竞争、性能瓶颈和GC问题等多种场景。工具之间还可以组合使用。例如,先用jstack找到问题线程,再用Arthas的jad反编译相关类;或者用JFR记录数据,再用JMC可视化分析。
在生产环境中,建议提前配置监控日志,部署Arthas并启用JFR,遇到问题时根据严重性选择重启或排查策略,确保服务稳定性和问题根因分析兼顾。希望本文的排查思路能为您解决JVM问题提供实用指导!
附录:系列目录
- JVM生产环境问题定位与解决实战(一):掌握jps、jmap、jstat、jstack、jcmd等基础工具
- JVM生产环境问题定位与解决实战(二):JConsole、VisualVM到MAT的高级应用
- JVM生产环境问题定位与解决实战(三):揭秘Java飞行记录器(JFR)的强大功能
- JVM生产环境问题定位与解决实战(四):使用JMC进行JFR性能分析指南
- JVM生产环境问题定位与解决实战(五):Arthas——不可错过的故障诊断利器
- ➡️ 当前:JVM生产环境问题定位与解决实战(六):总结篇——问题定位思路与工具选择策略
🔥 下篇预告:《JVM生产环境问题定位与解决实战(七):真实案例解剖——从工具链到思维链的完全实践》
🚀 关注作者,获取实时更新通知!有问题欢迎在评论区交流讨论~