JVM 线上故障排查基本操作
一、为什么要系统掌握 JVM 线上故障排查在 Java 后端系统的日常维护中开发者最担心的问题往往不是功能 Bug而是线上环境突然出现的性能骤降、内存持续增长、服务无响应等“非功能性”故障。这类问题具有三个明显特征一是它们通常只在生产环境的高并发、大数据量、长时间运行场景下才会暴露本地很难复现二是它们不会像空指针异常那样直接给出明确的错误堆栈而是表现为 CPU 飙升、GC 频繁、响应变慢等间接现象三是如果排查方向错误盲目重启或反复加机器往往只能暂时缓解问题却无法找到根因。JVM 作为 Java 程序的运行底座绝大多数线上性能故障最终都可以归结为 JVM 层面的资源使用异常。因此掌握一套系统化、可复用的 JVM 故障排查方法是每一名中高级 Java 工程师必备的能力。本文将从 JVM 内存结构与垃圾回收的基本原理出发重点介绍 JDK 自带命令行工具和 Arthas 等第三方工具的实际用法并结合 CPU 飙升、内存泄漏、频繁 Full GC、线程死锁等典型场景给出完整的排查思路与操作步骤。阅读建议本文内容较多既可作为系统学习材料也可作为线上故障处置手册。建议先通读第二部分建立方法论框架再结合具体故障类型查阅对应章节。文中的命令示例均可在 Linux 生产环境中直接使用。二、JVM 故障排查的通用方法论2.1 排查的基本流程面对任何 JVM 线上故障都不要急于下结论或盲目执行重启。一个行之有效的排查流程通常包含以下五步明确症状先搞清楚“发生了什么”是 CPU 使用率高、内存占用高、GC 停顿长、响应变慢还是进程直接退出或假死。收集现场在第一时间保留现场证据包括 GC 日志、线程堆栈、堆内存快照、操作系统层面的进程信息等。没有现场证据的排查往往只能靠猜测。定位过程根据症状选择合适的工具逐步缩小问题范围找到“是哪个线程、哪些对象、哪段代码、哪个配置”导致的异常。验证根因对初步结论进行交叉验证例如通过修改参数、复现、观察指标变化等方式确认根因。修复与沉淀完成代码或参数修复后补充监控告警、应急预案与复盘文档避免同类问题再次发生。需要特别强调的是“先观察、后动手”。很多事故的恶化不是因为问题本身有多大而是因为运维人员在不了解现场的情况下直接重启进程、清空日志或杀进程导致关键证据丢失问题变成“偶发但无解”的悬案。2.2 排查时的常见误区只看 CPU 和内存不看 GC 日志GC 日志是判断垃圾回收是否异常最直接的证据很多性能问题在 GC 日志里一眼就能看出瓶颈。只 dump 堆不 dump 线程内存问题看堆CPU 和死锁问题看线程两者不能互相替代。把 Full GC 当成唯一指标新生代 GC 频率过高同样会导致吞吐量下降不能只看 Full GC 次数。频繁抓取堆快照却不会分析堆 dump 文件动辄几个 GB抓取动作本身就会暂定应用如果没有后续分析能力反而加剧故障。忽视堆外内存和操作系统参数容器内存限制、直接内存、线程栈、元空间、代码缓存等都会占用内存超出堆内范围的问题无法靠堆分析发现。三、JVM 内存结构与故障类型概述3.1 JVM 运行时数据区理解故障排查的前提是理解 JVM 的内存布局。以最为常用的 HotSpot 虚拟机为例运行时数据区主要分为以下几部分区域是否线程私有主要作用常见异常程序计数器是记录当前线程执行的字节码位置一般不会异常虚拟机栈是存储方法调用的栈帧包含局部变量表、操作数栈等StackOverflowError本地方法栈是为本地方法Native 方法服务StackOverflowError堆 Heap否存放对象实例GC 的主要区域OutOfMemoryError: Java heap space元空间 Metaspace否存放类元数据信息OutOfMemoryError: Metaspace直接内存 Direct Memory否NIO 分配的非堆内存OutOfMemoryError: Direct buffer memory其中堆内存又被划分为新生代Young Generation和老年代Old Generation。新生代包含 Eden 区和两个 Survivor 区S0、S1。对象优先在 Eden 区分配经过多次 Minor GC 仍存活的对象会被晋升到老年代大对象可能直接进入老年代。3.2 常见故障类型的特征与映射把业务症状映射到 JVM 层面的根因是高效排查的关键。常见映射关系如下CPU 使用率持续 100%通常是某几个线程陷入死循环、密集计算或者是 GC 线程长期占用 CPU。需要查看线程堆栈定位具体代码。内存使用率持续上升直到 OOM通常是内存泄漏即对象被不断创建但无法被回收。需要对比多次堆快照找出持续增长的对象类型。响应时间周期性抖动通常是 GC 停顿导致需要分析 GC 日志判断是 Minor GC 频繁还是 Full GC 时间长。服务假死、请求无响应通常是线程池耗尽、死锁、长时间 STWStop The World或 IO 阻塞。需要查看线程状态与 GC 停顿。进程被系统 OOM Killer 杀死通常是整体内存堆 堆外 其他进程超过容器/系统限制而不是单纯的堆溢出。四、JVM 排查工具家族总览4.1 工具分类JDK 自带的排查工具可以分为命令行工具和图形化工具两大类此外还有以 Arthas、MAT 为代表的第三方工具。下面先用一个总表梳理各工具的用途后续章节再逐一详解核心命令的用法。工具类型主要用途典型场景jps命令行列出本机 Java 进程及 PID所有排查的第一步获取进程号jstat命令行监控堆内存、GC、类加载等指标快速判断 GC 是否异常、内存分代变化jinfo命令行查看和调整 JVM 参数、系统属性确认启动参数是否生效jmap命令行查看堆内对象统计、导出堆快照内存泄漏分析、OOM 现场留存jstack命令行打印线程堆栈CPU 飙升定位、死锁检测、线程状态分析jcmd命令行综合诊断命令覆盖 jmap/jstack 等功能现代 JDK 推荐的综合入口jhat命令行分析堆快照并启动 Web 服务轻量级堆分析生产建议用 MATjconsole图形化可视化监控 JVM 各项指标本地/远程图形监控jvisualvm图形化监控、线程分析、堆分析、抽样开发环境或可图形化环境排查Arthas第三方在线诊断支持反编译、热更新、trace无需重启进程的在线排查利器MAT第三方堆快照离线深度分析精确定位内存泄漏的引用链4.2 命令工具的前提条件上述命令行工具都位于 JDK 安装目录的bin目录下在环境变量配置正确时可直接使用。需要注意以下两点部分工具如 jmap、jstack需要通过attach 机制连接到目标 JVM 进程因此执行工具的用户必须与目标进程的用户相同或者具备相同权限。从 JDK 9 开始jmap 等工具的某些功能逐步被 jcmd 取代但为了兼容性本文仍以最常用的 JDK 8 行为为主进行介绍并补充 JDK 9 的差异。另外强烈建议在部署线上应用时提前开启 GC 日志和 OOM 自动 dump这样即使没有第一时间人工介入也能留下宝贵的现场数据。五、基础命令详解一jps 与进程定位5.1 jps 命令说明jpsJava Virtual Machine Process Status Tool用于列出当前机器上所有 HotSpot JVM 进程相当于 Java 世界的ps命令。它是所有排查操作的起点因为后续的 jstat、jmap、jstack 等命令都需要指定进程 IDPID。5.2 常用参数jps列出进程号与主类名。jps -l列出完整的包名 主类名或 Jar 包全路径。jps -m列出传递给 main 方法的参数。jps -v列出传递给 JVM 的参数可用于快速确认 Xms、Xmx 等配置。jps -q只输出进程号方便脚本使用。示例bash# 列出所有 Java 进程及主类名 jps -l # 列出进程并查看 JVM 启动参数 jps -v # 组合参数查看完整类名 JVM 参数 jps -l -v输出示例bash12345 com.example.order.OrderApplication -Xms4g -Xmx4g -XX:UseG1GC 23456 sun.tools.jps.Jps5.3 注意事项如果执行jps后看不到预期进程先检查进程是否运行在该用户权限下或者进程是否为非 HotSpot JVM。容器场景中如果需要查看宿主机的所有 Java 进程需要在与目标进程相同的命名空间中执行命令。某些安全加固环境会限制 attach 机制导致 jps 只能看到部分进程此时可以用ps -ef | grep java辅助确认。六、基础命令详解二jstat 与 GC 监控6.1 jstat 命令说明jstatJVM Statistics Monitoring Tool用于实时监控 JVM 的堆内存使用情况、垃圾回收状况、类加载和编译信息。它是排查 GC 问题时最轻量、最常用的工具因为它不需要暂停应用也不会产生大批量输出适合长时间、高频次采样。6.2 核心语法bashjstat -option pid [interval count]其中interval是采样间隔毫秒count是采样次数。不加这两个参数时只输出一次快照。6.3 常用选项选项作用-class监视类加载、卸载数量及耗时-gc查看堆各分区容量及 GC 统计最常用-gccapacity查看各分区的最大、最小、当前容量-gcutil查看各分区使用占比及 GC 时间占比最直观-gccause在 gcutil 基础上增加上次/本次 GC 的原因-gcnew查看新生代 GC 详情-gcold查看老年代 GC 详情-compiler查看 JIT 编译信息6.4 -gcutil 输出解读执行jstat -gcutil pid 1000 5后输出类似如下bashS0 S1 E O M CCS YGC YGCT FGC FGCT GCT 0.00 15.24 62.31 43.51 96.18 92.37 184 3.112 13 1.892 5.004 0.00 15.24 64.11 43.52 96.18 92.37 184 3.112 13 1.892 5.004 0.00 15.24 65.89 43.52 96.18 92.37 185 3.123 13 1.892 5.015各列含义如下S0/S1两个 Survivor 区的使用百分比。正常情况下一方为 0 或很低另一方在对象复制时变化。EEden 区使用百分比。通常在对象快速创建时波动明显。O老年代使用百分比。若持续上升且回收后仍不下降基本可判定存在内存泄漏。M元空间使用百分比。若接近 100%可能存在类加载泄漏或动态生成类过多。CCS压缩类空间使用百分比。YGC/YGCT新生代 GC 次数与总耗时。FGC/FGCTFull GC 次数与总耗时。FGC 频繁增长是强烈告警信号。GCTGC 总耗时。6.5 实战判断方法bash# 每 1 秒输出一次共输出 20 次观察变化趋势 jstat -gcutil 12345 1000 20 # 同时查看 GC 原因 jstat -gccause 12345 1000 20判断要点如果 O 老年代使用率呈现“周期性增长又回落到低位”说明 GC 工作正常只是对象晋升速度较快。如果 O 持续增长、即使发生 Full GC 也回落不明显说明存在内存泄漏需要 dump 堆进一步分析。如果 YGC 次数增长非常快而 FGC 次数并不多说明新生代空间可能过小对象来不及在新生代回收就被晋升可考虑调大新生代容量或调整-Xmn。如果 FGC 频繁增长且 FGCT 持续上升说明老年代压力较大或出现了较长时间的系统级停顿需要结合 GC 日志确认 Full GC 的触发原因。如果 GCT 占运行时间的比例已经达到百分之几甚至更高说明应用大量时间花在 GC 上需要优化对象分配速度或选择合适的垃圾回收器。七、基础命令详解三jinfo 与 JVM 参数确认7.1 jinfo 命令说明jinfoConfiguration Info for Java用于查看和动态调整 JVM 运行参数以及系统属性。线上排查时它最常见的用途是快速确认应用“实际生效”的启动参数判断-Xmx、-Xms、垃圾回收器等配置是否与预期一致避免出现“配置漂移”导致排查方向被误导。7.2 常用参数jinfo pid同时查看 JVM 参数和系统属性。jinfo -flags pid只看 JVM 启动参数。jinfo -sysprops pid只看系统属性。jinfo -flag name pid查看指定参数值如jinfo -flag MaxHeapSize 12345。jinfo -flag [|-]name pid动态开启或关闭布尔参数如jinfo -flag PrintGCDetails 12345。jinfo -flag namevalue pid动态修改可调整参数。7.3 实战示例bash# 查看目标进程实际生效的所有 JVM 启动参数 jinfo -flags 12345 # 查看当前最大堆内存配置 jinfo -flag MaxHeapSize 12345 # 查看当前使用的垃圾回收器 jinfo -flag UseG1GC 12345输出示例textVM Flags: -XX:InitialHeapSize268435456 -XX:MaxHeapSize4294967296 -XX:UseG1GC -XX:UseCompressedClassPointers -XX:UseCompressedOops需要提醒的是通过jinfo -flag动态修改参数虽然方便但并非所有参数都支持运行期调整而且动态调整可能引入新的不确定性。生产环境应优先通过修改启动参数并滚动重启的方式固化配置而不是依赖运行期临时修改。八、基础命令详解四jmap 与堆内存分析8.1 jmap 命令说明jmapMemory Map用于查看堆内存使用详情、统计堆内对象分布并导出堆快照Heap Dump。它是内存泄漏分析和 OOM 现场留存的核心工具。8.2 常用参数jmap -heap pid查看堆配置和各区域使用概况。jmap -histo[:live] pid查看堆内对象直方图使用 live 时会先触发一次 Full GC只统计存活对象。jmap -dump:formatb,fileheap.hprof pid导出堆快照到文件。jmap -finalizerinfo pid查看等待执行 finalize 的对象信息。jmap -clstats pid查看类加载器统计信息。8.3 堆直方图分析当怀疑内存持续增长时jmap -histo是最快定位问题对象的入口bash# 触发一次 Full GC 后查看存活对象避免把可回收垃圾算进去 jmap -histo:live 12345 # 只显示占用内存最多的前 20 个类 jmap -histo:live 12345 | head -20 # 按内存占用从大到小排序 jmap -histo:live 12345 | sort -k3 -nr | head -30输出各列依次为编号、对象实例数量、对象占用总字节数、类名。实战中应重点观察自定义业务类的实例数是否异常膨胀例如com.example.order.OrderDTO出现几百万个实例。集合类底层数组是否过大例如大量byte[]、char[]、Object[]通常对应缓存、序列化或大字符串。是否出现大量同类加载器产生的重复类实例这往往与动态代理、热部署或反射滥用有关。8.4 导出堆快照bash# 标准方式二进制格式保存到指定文件 jmap -dump:formatb,file/data/dump/heap_20260925.hprof 12345 # 自动转储应用发生 OOM 时自动导出建议提前配置 -XX:HeapDumpOnOutOfMemoryError -XX:HeapDumpPath/data/dump/oom.hprof8.5 注意事项导出大堆快照本身会造成较长停顿尤其在使用 G1 等收集器时更明显非必要不要在业务高峰期执行。部分容器化环境中JDK 的 attach 机制可能受限导致jmap -heap失败此时可改用jcmd的对应子命令。JDK 9 及更高版本如果目标进程无法直接 attach可使用jhsdb jmap作为替代。获取到堆快照后建议先用 jhat、MAT 或 VisualVM 做离线分析不要继续在生产机上执行。生产环境优先用 MAT 做深入分析。九、基础命令详解五jstack 与线程分析9.1 jstack 命令说明jstackStack Trace用于打印 JVM 内所有线程的堆栈信息。它是排查 CPU 飙升、线程死锁、线程假死和线程池耗尽等问题的主要工具。9.2 常用参数jstack pid打印所有线程堆栈。jstack -l pid附加显示锁信息可用于分析 synchronized 锁竞争和可重入锁。jstack -F pid当进程无响应、普通模式无法正常输出时强制打印线程快照。jstack -m pid混合模式同时打印 Java 帧和本地方法帧。9.3 线程状态解读在 jstack 输出中每个线程上方都会标注状态常见状态含义如下状态含义常见原因RUNNABLE正在运行或等待 CPU 调度正常计算、自旋、密集循环WAITING无限期等待需要其他线程显式唤醒Object.wait、LockSupport.park、joinTIMED_WAITING有限时间等待超时自动唤醒Thread.sleep、带超时的 wait/parkBLOCKED等待获取 synchronized 锁锁竞争、死锁或临界区过长NEW线程已创建但尚未启动很少出现在线上堆栈中TERMINATED线程已结束一般不会出现在运行中的堆栈里大量线程长期处于 BLOCKED 状态说明存在明显的锁竞争大量线程处于 WAITING 或 TIMED_WAITING 且不恢复往往与线程池耗尽、依赖下游超时或资源等待有关。9.4 死锁检测如果 jstack 输出底部出现如下提示说明检测到了死锁应结合对应线程堆栈逐条确认锁的获取顺序textFound one Java-level deadlock: Thread-1: waiting to lock monitor 0x00007f9a140038e0 (object 0x00000000f39c2c18, a java.lang.Object), which is held by Thread-0 Thread-0: waiting to lock monitor 0x00007f9a14003230 (object 0x00000000f39c2c28, a java.lang.Object), which is held by Thread-1 Java stack information for the threads listed above: 9.5 CPU 飙升定位步骤bash# 第 1 步找到系统中 CPU 占用最高的 Java 线程 top -Hp 12345 # 第 2 步把该线程号从十进制转为十六进制例如 32579 转为 0x7f43 printf %x\n 32579 # 第 3 步在 jstack 输出中搜索该十六进制线程号 jstack 12345 | grep -A 30 0x7f43通过上述三步即可定位到具体代码位置。若多个线程都在执行同一段业务逻辑需要结合代码进一步判断是算法复杂度过高、死循环还是一次性处理的数据量过大。十、基础命令详解六jcmd 综合诊断10.1 jcmd 命令说明jcmd 是 JDK 提供的综合性诊断命令。与 jmap、jstack 等分散命令相比它通过“主命令 子命令”的方式把常用诊断能力统一起来是 JDK 8 之后官方推荐的综合入口。10.2 常用子命令子命令作用jcmd pid help列出该进程支持的诊断子命令jcmd pid VM.version查看 JVM 版本信息jcmd pid VM.flags查看全部启动参数jcmd pid VM.system_properties查看系统属性jcmd pid Thread.print打印线程堆栈等价于 jstackjcmd pid GC.heap_dump file.hprof导出堆快照jcmd pid GC.class_histogram查看堆内对象直方图jcmd pid GC.run执行一次垃圾回收10.3 实战示例bash# 确认进程实际启动参数 jcmd 12345 VM.flags # 查看线程快照 jcmd 12345 Thread.print # 导出堆快照功能对标 jmap -dump jcmd 12345 GC.heap_dump /data/dump/heap.hprof相比传统工具jcmd 更加统一、易扩展推荐优先使用尤其是在新版本 JDK 中。十一、GC 日志分析与 JVM 参数优化11.1 为什么要看 GC 日志GC 日志能够记录每次垃圾回收的起止时间、触发原因、各区域的回收情况以及停顿时间。很多性能问题在“CPU 正常、内存也没有打满”的情况下只有通过 GC 日志才能发现真正瓶颈例如单次 Young GC 耗时过长、Full GC 由元空间扩容触发、晋升速度过快等。11.2 GC 日志参数配置JDK 8 常用配置如下bash-XX:PrintGCDetails -XX:PrintGCDateStamps \ -Xloggc:/data/logs/gc-%t.log \ -XX:UseGCLogFileRotation \ -XX:NumberOfGCLogFiles10 \ -XX:GCLogFileSize50MJDK 9 及更高版本推荐使用统一日志框架bash-Xlog:gc*:file/data/logs/gc.log:time,uptime,level,tags:filecount10,filesize50M11.3 GC 日志解读要点以 JDK 8 的 Parallel GC 为例一段典型日志如下text2026-09-25T20:10:11.5230800: 1234.567: [Full GC (Allocation Failure) 2026-09-25T20:10:11.5230800: 1234.567: [PSYoungGen: 102400K-0K(114688K)] [ParOldGen: 655360K-409600K(699392K)] 757760K-409600K(814080K), [Metaspace: 78321K-78321K(110592K)], 0.4123400 secs]重点关注Allocation Failure触发原因指对象分配失败导致 GC。757760K → 409600K回收前后堆占用回收后是否明显下降是判断泄漏的重要依据。0.4123400 secs本次 GC 耗时长时间 Full GC 会导致明显停顿。11.4 常用 JVM 参数速查参数作用-Xms初始堆大小-Xmx最大堆大小-Xmn新生代大小-XX:SurvivorRatioEden 与 Survivor 比例-XX:MetaspaceSize元空间初始大小-XX:MaxMetaspaceSize元空间最大大小-XX:MaxDirectMemorySize直接内存最大值-Xss每线程栈大小以上参数是排查和调优 GC 问题最常打交道的配置项。实际排查中建议先在启动参数中开启 GC 日志和 OOM 自动 dump再结合 jstat、jmap、jstack 等工具综合分析避免仅凭单一指标下结论。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →