JVM问题排查流程
JVM 问题排查是开发和运维中的核心技能,涉及内存、线程、CPU、GC 等多个维度。以下是一套系统化的 JVM 问题排查流程,涵盖常见场景(堆内存溢出、线程泄露、CPU 飙高、GC 异常等)的排查步骤和工具使用:
一、问题初步定位:确定现象与范围
收集基础信息
- 问题发生时间、频率(偶发 / 必现)、是否与业务峰值相关。
- 应用部署环境:JDK 版本(
java -version)、操作系统(uname -a)、容器化与否(Docker/K8s)、资源限制(内存 / CPU 配额)。 - 应用日志:查看应用
stdout、业务日志中是否有异常(OutOfMemoryError、StackOverflowError、线程阻塞信息等)。 - 监控告警:结合监控平台(Prometheus + Grafana、SkyWalking 等),确认是否有内存飙升、CPU 持续高位、GC 耗时过长等指标异常。
初步判断问题类型
- 堆内存问题:OOM 日志、内存使用率持续上涨、GC 频繁。
- 线程问题:线程数暴增、死锁、线程阻塞(如
synchronized/ 锁竞争)。 - CPU 问题:CPU 使用率飙升(用户态 / 内核态)、特定线程占用过高。
- GC 问题:GC 停顿时间过长、Full GC 频繁、老年代 / 元空间溢出。
二、堆内存溢出(OOM: Java heap space)排查流程
现象:应用抛出 java.lang.OutOfMemoryError: Java heap space,或堆内存使用率接近 100% 且无法回收。
排查步骤:
确认是否开启堆转储(Heap Dump)
- 启动时添加参数:
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/to/dump.hprof(OOM 时自动生成堆快照)。 - 若未开启,手动生成:
jmap -dump:format=b,file=heap.hprof <pid>(pid为进程 ID,可通过jps或ps -ef | grep java获取)。
- 启动时添加参数:
分析堆快照
- 工具:MAT(Eclipse Memory Analyzer Tool)、JProfiler、VisualVM。
- 关键指标:
- 支配树(Dominator Tree):找出占用内存最多的对象(如大集合、未释放的缓存)。
- 泄漏可疑点(Leak Suspects):MAT 自动分析可能的内存泄漏源(如静态集合持有大量对象引用)。
- 对象引用链:查看大对象被哪些对象引用(是否有长生命周期对象持有短生命周期对象)。
结合代码定位问题
- 常见原因:
- 集合未及时清理(如
HashMap/List持续添加元素未删除)。 - 缓存设计不合理(如无过期策略的本地缓存)。
- 大对象生成(如一次性加载大量数据到内存)。
- 集合未及时清理(如
- 验证:通过日志或调试,确认可疑对象的创建和销毁逻辑是否存在漏洞。
- 常见原因:
临时解决方案
- 紧急扩容堆内存:
-Xms<初始堆> -Xmx<最大堆>(如-Xms4g -Xmx4g),但需结合分析结果优化代码,否则问题会复现。
- 紧急扩容堆内存:
三、线程问题排查(死锁、线程泄露、阻塞)
现象:应用响应变慢、线程数持续增加、日志出现锁等待超时(如 java.util.concurrent.TimeoutException)。
排查步骤:
查看线程状态
- 生成线程快照:
jstack <pid> > thread_dump.txt(多次执行,对比线程状态变化)。 - 工具分析:VisualVM(线程面板)、Thread Dump Analyzer(TDA)。
- 生成线程快照:
常见线程问题分析
- 死锁:
- 线程快照中搜索
deadlock关键字,会显示互相持有锁的线程及锁信息(如java.lang.Thread.State: BLOCKED)。 - 示例:线程 A 持有锁 L1 等待 L2,线程 B 持有锁 L2 等待 L1。
- 线程快照中搜索
- 线程泄露:
- 线程数远超正常范围(
jstack中线程名重复且状态为RUNNABLE/TIMED_WAITING)。 - 原因:线程池未正确关闭、创建大量无界线程(如
new Thread()未回收)。
- 线程数远超正常范围(
- 锁竞争:
- 大量线程处于
BLOCKED状态,等待同一把锁(如synchronized修饰的热点方法)。 - 可通过
jstack统计阻塞线程数,或使用jconsole监控锁竞争次数。
- 大量线程处于
- 死锁:
定位代码
- 根据线程快照中的栈信息(类名、方法名、行号),找到对应的代码位置。
- 死锁:检查锁的获取顺序是否一致;线程泄露:检查线程创建 / 销毁逻辑;锁竞争:优化锁粒度(如使用
ReentrantLock或减少锁范围)。
四、CPU 使用率飙高排查
现象:top 命令显示 Java 进程 CPU 使用率 > 80%,应用响应卡顿。
排查步骤:
定位高 CPU 线程
- 步骤 1:用
top -p <pid>查看进程 CPU 使用率,按H显示线程列表,找到占用 CPU 最高的线程 ID(TID,十进制)。 - 步骤 2:将线程 ID 转为十六进制(如
printf "%x\n" 1234),用于后续搜索。 - 步骤 3:生成线程快照:
jstack <pid> | grep <十六进制TID> -A 30(查看该线程的栈信息)。
- 步骤 1:用
分析线程栈
- 若线程状态为
RUNNABLE:- 可能是无限循环(如
while(true)未正确退出)、密集计算(如大量正则匹配、循环逻辑低效)。
- 可能是无限循环(如
- 若线程状态为
RUNNABLE且涉及 IO:- 可能是频繁磁盘 IO 或网络 IO(如同步 RPC 调用阻塞)。
- 若多个线程 CPU 均高:
- 可能是锁竞争激烈(线程在
RUNNABLE和BLOCKED间频繁切换)。
- 可能是锁竞争激烈(线程在
- 若线程状态为
验证与优化
- 结合代码确认是否存在低效逻辑(如 O (n²) 算法),或通过压测复现问题。
- 优化方案:修复无限循环、优化算法、异步化 IO 操作、减少锁竞争等。
五、GC 异常排查(频繁 GC、停顿过长)
现象:GC 日志显示 Full GC 频繁(每秒数次)、单次 GC 停顿 > 1s,或老年代 / 元空间持续增长。
排查步骤:
开启并分析 GC 日志
- 启动参数开启 GC 日志:
bash
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC -Xloggc:/path/to/gc.log - 工具分析:GCViewer、GCEasy(在线工具)、VisualVM。
- 启动参数开启 GC 日志:
关键指标判断
- Young GC 频繁:Eden 区过小,或对象创建速度快(如短生命周期对象过多)。
- Full GC 频繁:
- 老年代空间不足(大对象直接进入老年代,或对象晋升过快)。
- 元空间溢出(
-XX:MetaspaceSize配置过小,或类加载过多未卸载)。
- GC 停顿过长:
- 老年代过大(Full GC 扫描时间长),或使用 Serial GC(单线程 GC,适合小堆)。
优化方案
- 调整堆参数:增大 Eden 区(
-Xmn)、调整老年代比例、增大元空间(-XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m)。 - 更换 GC 收集器:高并发场景用 G1(
-XX:+UseG1GC),超大堆用 ZGC/Shenandoah(JDK11+)。 - 代码优化:减少大对象创建、避免静态集合内存泄漏(减少老年代占用)。
- 调整堆参数:增大 Eden 区(
六、元空间溢出(OOM: Metaspace)排查
现象:抛出 java.lang.OutOfMemoryError: Metaspace,通常与类加载相关。
排查步骤:
确认元空间配置
- 检查启动参数:
-XX:MetaspaceSize(初始阈值,默认 21MB)、-XX:MaxMetaspaceSize(默认无上限,受限于物理内存)。
- 检查启动参数:
分析类加载情况
- 工具:
jmap -clstats <pid>(查看类加载统计,包括加载数、卸载数)。 - 常见原因:
- 动态生成类过多(如 CGLib 代理、反射频繁生成类)。
- 类加载器泄露(如自定义类加载器未被回收,导致其加载的类无法卸载)。
- 工具:
优化
- 增大元空间:
-XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m。 - 限制动态类生成数量,或复用类加载器。
- 增大元空间:
七、总结:通用排查工具链
| 工具 / 命令 | 用途 | 关键参数 / 操作 |
|---|---|---|
jps | 查看 Java 进程 ID | jps -l(显示进程全类名) |
jstat | 监控 JVM 统计信息(GC、类加载) | jstat -gcutil <pid> 1000(每秒输出 GC 占比) |
jmap | 生成堆快照、查看内存分布 | jmap -dump:format=b,file=heap.hprof <pid> |
jstack | 生成线程快照,排查死锁 / 线程阻塞 | jstack <pid> > thread.txt |
top/jstack | 定位高 CPU 线程 | top -p <pid> -> H -> 线程 ID 转十六进制 |
MAT/VisualVM | 分析堆快照,定位内存泄漏 | 支配树、泄漏可疑点分析 |
GCViewer | 分析 GC 日志,优化 GC 配置 | 导入 gc.log 生成可视化报告 |
核心原则
- 先监控后动手:通过日志和监控工具定位问题范围,避免盲目调整参数。
- 保留现场:发生问题时优先收集堆快照、线程快照、GC 日志,便于后续分析。
- 结合代码:工具仅能定位现象,最终需通过代码逻辑找到根本原因(如内存泄漏的引用链、死锁的锁顺序)。
- 避免 “试错式” 优化:参数调整(如堆大小、GC 收集器)需基于分析结果,而非随机尝试。
