← 返回文章

JVM 问题排查

发布于 2026/10/19

1. 完整回答

JVM 问题排查主要是针对 Java 应用运行过程中出现的 CPU 使用率过高、内存持续增长、频繁 GC、接口响应缓慢、线程阻塞以及内存溢出 等问题,通过监控指标和诊断工具定位根本原因。

实际排查一般遵循 先观察现象、再缩小范围、最后定位根因 的思路。

首先,通过监控系统查看 CPU、内存、GC 次数和耗时、线程数量以及接口响应时间,结合应用日志判断异常发生的时间和影响范围。

其次,根据问题类型选择工具。如果 CPU 使用率过高,可以通过 top 找到高 CPU 线程,再结合 jstack 或 jcmd 查看线程堆栈,判断是否存在死循环、热点计算或者频繁 GC。

如果内存持续增长,可以通过 jstat 和 GC 日志观察堆使用情况以及垃圾回收效果,必要时使用 jmap 或 jcmd 导出堆转储文件,再通过 MAT 分析对象数量、引用关系和内存占用。

如果接口响应缓慢,可以分析线程状态,检查锁竞争、线程池耗尽、数据库连接池等待以及外部接口调用是否存在阻塞。

最后,根据定位结果采取针对性的措施,比如修复内存泄漏、优化热点代码、调整线程池配置,或者在确认原因后优化 JVM 参数,并通过监控验证效果。

整个过程最重要的是 结合业务现象、系统指标和 JVM 运行时证据进行判断,避免仅凭单一指标直接调整参数。

2. 核心原理与关键概念

2.1 核心排查流程

第一步:确认异常范围

业务告警 → 查看 CPU、内存、GC、线程和接口耗时 → 对比正常时段 → 判断异常属于计算、内存、垃圾回收还是线程等待问题。

这里需要结合请求量、发布时间和异常日志,避免把正常的业务流量增长误判为 JVM 故障。

第二步:采集运行时证据

根据异常类型选择对应工具:

  • CPU 高 → 定位高 CPU 线程 → 分析线程堆栈或 CPU 采样。

  • 内存高 → 查看 GC 后内存变化 → 分析对象分布和引用关系。

  • GC 频繁 → 分析 GC 日志 → 判断分配速率、回收效果和停顿时间。

  • 请求卡顿 → 分析线程堆栈 → 定位锁等待、资源池等待或外部调用。

  • 进程退出 → 检查异常日志、容器事件及 JVM 崩溃日志。

第三步:定位根因并验证

异常指标 → 关联具体线程、对象或代码路径 → 找到触发原因 → 修复或调整配置 → 对比修复前后的 CPU、GC、内存和响应时间。

2.2 常用诊断工具

工具 主要作用 典型场景
top、top -H 查看进程和线程的 CPU 占用 CPU 飙高
jps 查找 Java 进程 确认 JVM 进程 ID
jstat 观察堆使用率和 GC 统计 GC 频繁、内存异常
jcmd 查看线程、堆、JVM 配置等诊断信息 综合故障排查
jstack 导出线程堆栈 死锁、线程阻塞
jmap 查看对象统计、导出堆转储 内存泄漏分析
JFR 采集 CPU、分配、GC、锁等运行事件 持续性能分析
MAT 分析堆转储中的对象引用和保留内存 定位内存泄漏

现代 HotSpot JVM 通常优先使用 jcmd 执行诊断命令,jstack、jmap 仍然可以使用。

2.3 关键分析指标

指标 需要关注的现象
CPU 使用率 是否持续偏高,是否集中在少数线程
堆内存使用量 GC 后的存活对象占用是否持续增长
Young GC 发生频率是否异常,单次停顿是否过长
Full GC 是否频繁发生,回收后释放了多少内存
GC 停顿时间 是否影响接口延迟和业务吞吐量
线程数量和状态 是否大量阻塞、等待或持续增长
进程实际内存(RSS) 是否超过预期,是否接近容器内存限制

其中,GC 后存活对象的长期变化、GC 总耗时以及异常线程堆栈,通常比单次内存使用率或 CPU 峰值更有判断价值。

3. 高频面试追问

3.1 线上 Java 进程 CPU 使用率达到 100%,怎么排查?

首先确认高 CPU 是否来自目标 Java 进程,然后定位具体线程,最后通过线程堆栈和性能采样找到热点代码。

排查流程:

  1. 使用 top 找到 CPU 占用较高的 Java 进程,确定 PID。

  2. 使用 top -H -p PID 查看进程内部线程的 CPU 占用,找到异常线程的线程 ID。

  3. 将线程 ID 转换成十六进制,与线程堆栈中的 nid 对应。

  4. 使用 jcmd PID Thread.print -l 或 jstack 获取线程堆栈,检查异常线程正在执行的方法。

  5. 连续采集多次堆栈,必要时结合 JFR 的 CPU 采样,确认热点代码。

如果线程长期执行相同的计算逻辑,可能存在死循环、低效算法或者大量重复计算。

如果 CPU 主要消耗在 GC 线程上,就需要结合 GC 日志检查是否存在频繁垃圾回收。

关键是同时确认 CPU 消耗来自哪里,以及对应线程执行了什么操作。 单次线程堆栈只能反映一个瞬间,连续采样更容易判断问题。

3.2 JVM 堆内存持续上涨,怎么判断是否发生内存泄漏?

判断内存泄漏,主要看 GC 后仍然存活的对象是否持续增长,以及这些对象是否还被业务需要。

Java 堆内存本身就会随着对象创建而上涨,垃圾回收后下降,这种周期性变化属于正常现象。

如果在业务负载相对稳定的情况下,经过多轮有效垃圾回收,存活对象占用仍然持续增长,就需要怀疑存在内存泄漏。

具体排查流程:

  1. 通过监控和 GC 日志观察堆内存曲线,重点比较不同 GC 周期后的存活对象占用。

  2. 使用 jcmd GC.class_histogram 查看对象数量和占用分布,必要时导出 Heap Dump。

  3. 使用 MAT 分析对象的保留内存(Retained Heap),找出占用大量内存的对象集合。

  4. 通过 GC Roots 引用链,定位哪些对象仍然持有这些数据。

  5. 结合代码检查缓存、静态集合、监听器或 ThreadLocal 是否长期持有已经失效的对象。

例如,一个本地缓存不断加入数据,却没有设置容量限制或者淘汰策略,缓存对象始终可达,就可能导致堆内存持续增长。

继续追问:为什么 Java 有 GC,仍然会发生内存泄漏?

因为 GC 主要根据对象的可达性判断能否回收。如果对象已经没有业务价值,但仍然被静态集合、线程局部变量或者其他存活对象引用,GC 就无法回收它。

3.3 线上出现 OutOfMemoryError,应该怎么排查?

出现 OOM 时,首先查看具体错误信息,判断是哪一类内存资源耗尽,再选择对应的诊断方式。

常见情况包括:

错误或现象 可能原因 排查方向
Java heap space 堆空间不足、大量对象存活 GC 日志、Heap Dump
GC overhead limit exceeded GC 消耗大量时间,回收效果很差 GC 时间与回收量
Metaspace 类元数据占用过高、类加载器未释放 类加载数量、类加载器引用
Direct buffer memory 直接缓冲区分配超出限制 直接内存分配及释放情况
unable to create native thread 线程数、进程限制或本地内存不足 线程数量、系统资源限制
容器 OOMKilled 容器内存使用量超过限制 容器事件、内存指标

如果是堆内存 OOM,可以通过 -XX:+HeapDumpOnOutOfMemoryError 在相应 OOM 发生时自动生成堆转储,再使用 MAT 分析大对象及引用链。

如果是本地内存问题,就需要分析直接内存、线程栈、元空间以及其他本地内存分配。

另外,容器被操作系统 OOM Killer 终止,不一定会产生 Java 的 OutOfMemoryError,也可能没有 Heap Dump,这种情况要结合容器状态和系统日志判断。

解决时还需要区分内存配置不足、瞬时峰值过高和真实内存泄漏。单纯增加堆大小可能暂时缓解问题,但无法修复对象长期无法回收的情况。

3.4 Young GC 或 Full GC 频繁,怎么排查?

GC 频繁主要从 对象分配速率、内存回收效果、GC 触发原因以及停顿时间 四个方面分析。

首先通过 jstat -gcutil PID 1000 或 GC 日志观察回收频率、老年代占用和 GC 耗时。

如果 Young GC 频繁,但每次停顿很短、回收效果良好,可能只是短生命周期对象创建速度较快,需要结合吞吐量和业务延迟判断是否真的存在问题。

如果 Full GC 频繁,就需要重点检查老年代空间压力、对象晋升情况和每次 GC 后的内存占用。

排查时通常关注:

  • 对象分配过快:短时间产生大量临时对象,使年轻代频繁回收。

  • 长期存活对象过多:缓存、集合或业务对象长期占据堆空间。

  • 内存配置不合理:堆空间不足或者收集器参数与业务负载不匹配。

  • 回收效率低:Full GC 后仍然有大量存活对象,甚至出现分配失败。

以 G1 为例,Mixed GC 会回收年轻代和部分老年代区域;如果正常的回收过程无法及时释放足够空间,严重时可能退化为 Full GC。

继续追问:增加 JVM 堆内存能减少 GC 吗?

在堆空间确实偏小的情况下,适当扩大堆可以降低内存压力,减少部分 GC 的发生频率。但如果存在内存泄漏或者对象分配过快的问题,增加堆大小只能缓解症状,还可能增加内存成本,因此需要先确认根因。

3.5 接口响应很慢,但是 CPU 使用率不高,怎么排查?

这种情况通常优先考虑线程等待或外部资源瓶颈,例如锁竞争、数据库连接池耗尽、网络 I/O 阻塞或者下游服务响应缓慢。

首先通过链路追踪和接口日志定位耗时较长的调用环节,然后采集线程堆栈,查看业务线程具体等待在哪里。

如果大量线程处于 BLOCKED 状态,需要检查锁竞争。

如果线程停留在连接池获取连接、Future.get() 或网络读取等位置,就需要继续检查相关资源池、异步任务和下游服务。

同时,还要检查线程池活跃线程数、队列长度、数据库连接池使用率和超时配置。

例如,数据库连接池最大只有 20 个连接,而慢 SQL 长时间占用连接,就可能导致大量请求线程排队。此时即使 CPU 使用率不高,接口仍然可能大面积超时。

3.6 如何通过线程堆栈定位死锁和线程阻塞?

主要通过线程状态、调用栈、锁持有关系以及多次采样结果定位。

使用 jcmd PID Thread.print -l 获取线程信息后,重点观察 BLOCKED、WAITING 和 TIMED_WAITING 状态的业务线程。

如果大量线程处于 BLOCKED,需要查看正在等待的锁,以及持有该锁的线程正在执行什么操作。

如果存在死锁,典型情况是线程 A 持有锁 1、等待锁 2,线程 B 持有锁 2、等待锁 1,形成循环等待。JVM 的线程诊断工具可以识别部分死锁并输出相关线程和锁信息。

如果没有检测到死锁,但大量请求长期无法继续执行,就需要检查资源池等待、网络调用和线程之间的依赖关系。

继续追问:线程处于 WAITING 状态就代表发生了问题吗?

不一定。线程池中的空闲工作线程可能正常处于 WAITING 状态。判断是否异常,要结合线程职责、等待位置、持续时间以及业务请求是否受到影响。

3.7 Java 堆内存使用正常,但是进程内存很高,是什么原因?

这种情况需要重点检查堆外内存和 JVM 本地内存。

Java 进程占用的内存除了 Java 堆,还包括元空间、线程栈、直接内存、代码缓存以及 JVM 自身使用的本地内存。

因此,-Xmx 限制的是 Java 堆的最大大小,不能直接代表整个 Java 进程的内存上限。

排查时,首先对比 JVM 堆使用情况和操作系统中的进程实际内存占用,然后检查线程数量、类加载数量和直接内存使用情况。

如果 JVM 启动时启用了 Native Memory Tracking,可以通过:

jcmd PID VM.native_memory summary

查看 JVM 内部不同类别的本地内存使用情况。

需要注意,NMT 必须提前启用,而且无法覆盖所有第三方本地库或 JNI 分配的内存;它统计的保留内存、已提交内存也不能直接等同于进程 RSS。

如果 JVM 堆和内部本地内存都无法解释增长,就需要进一步使用操作系统工具检查内存映射和本地内存分配。

3.8 线上排查 JVM 问题时,怎样避免影响正在运行的服务?

线上排查应当先使用监控和日志,再使用运行时诊断工具,最后才考虑高开销的数据采集。

例如,查看 CPU、GC 日志和已有 JFR 数据,通常适合作为初步排查手段。

线程堆栈可以帮助定位阻塞问题,但在线程数量很大时,采集过程也会产生一定开销。

Heap Dump 和对象直方图的开销更明显。尤其是 jcmd GC.heap_dump,默认会请求 Full GC,并且可能带来较长停顿、大量磁盘写入和额外资源消耗。

所以在线上导出大堆文件前,需要先确认磁盘空间、业务流量以及服务是否能够承受停顿,必要时选择低峰期或隔离实例操作。

同时,建议提前配置 GC 日志、合理的 JFR 持续记录,以及 -XX:+HeapDumpOnOutOfMemoryError,并准备足够的存储空间。采集文件还可能包含敏感业务数据,需要做好访问控制。

核心原则是先保留现场,再根据证据选择诊断手段,控制排查操作对业务的影响。

3.9 如果线上出现一次 JVM 性能故障,如何完整定位并解决?

以订单查询服务出现 CPU 持续偏高、接口响应时间明显增长为例。

首先查看监控,确认故障期间请求量没有明显增加,但 CPU 使用率和 Young GC 频率同时上升。

随后通过 top -H 和线程堆栈分析,发现 GC 线程消耗了大量 CPU,再结合 GC 日志发现年轻代回收频繁,但每次回收后内存能够明显下降。

这个现象说明短生命周期对象分配过快的可能性较高。

接着使用 JFR 分析对象分配情况,假设最终发现订单查询接口在循环中反复进行 JSON 序列化和对象转换,产生大量临时对象。

处理时,可以减少循环中的重复转换,复用必要的中间结果,并对查询逻辑进行针对性优化。

优化上线后,继续对比相同负载下的对象分配速率、GC 频率、CPU 使用率和接口 P95、P99 延迟。

如果这些指标明显改善,并且经过持续观察没有出现新的内存问题,就能形成比较完整的故障定位和验证闭环。

4. 易混淆概念对比

4.1 内存泄漏 vs 对象分配过快

对比维度 内存泄漏 对象分配过快
核心原因 无用对象仍被引用 短时间创建大量对象
GC 后表现 存活对象占用可能持续增长 多数临时对象能够回收
常见现象 老年代压力增加、可能 OOM Young GC 频繁、CPU 开销增加
处理方式 找到引用链并释放无效引用 减少不必要的对象创建

4.2 Java 堆内存 vs 进程实际内存

对比维度 Java 堆内存 进程实际内存(RSS)
范围 JVM 管理的对象堆空间 操作系统统计的进程驻留物理内存
主要内容 Java 对象和数组 堆、本地内存、线程栈及相关驻留页面等
主要工具 jstat、Heap Dump、MAT top、pmap、NMT 辅助分析
关键区别 受 -Xmx 限制 可能明显大于 Java 堆的实际使用量

4.3 Young GC vs Mixed GC vs Full GC

以下以 G1 垃圾收集器为例。

对比维度 Young GC Mixed GC Full GC
回收范围 年轻代 年轻代和部分老年代区域 整个 Java 堆
主要目的 回收短生命周期对象 逐步回收老年代空间 进行全堆回收与整理
停顿特点 通常较短 受回收区域数量等因素影响 通常开销较大
排查关注 分配速率、年轻代大小 老年代回收效果 触发原因、回收效果和停顿时间

G1 的 Mixed GC 有助于分阶段回收老年代,减少对代价较高的 Full GC 的依赖。

4.4 RUNNABLE vs BLOCKED vs WAITING

线程状态 含义 排查方向
RUNNABLE 可运行或正在执行,也可能处于部分本地 I/O 操作中 结合 CPU 指标分析热点代码
BLOCKED 等待获取 Java 对象监视器锁 锁竞争、锁持有者
WAITING 无限期等待某个条件或其他线程操作 任务依赖、线程池、条件等待
TIMED_WAITING 带超时时间的等待 超时设置、重试、定时等待

线程状态只能说明当时的执行情况,判断是否存在故障还需要结合具体堆栈。

4.5 Thread Dump vs Heap Dump vs JFR

对比维度 Thread Dump Heap Dump JFR
记录内容 线程状态和调用堆栈 堆中对象及引用关系 一段时间内的 JVM 运行事件
主要用途 死锁、阻塞、线程卡顿 内存泄漏、大对象分析 CPU、GC、分配、锁和 I/O 分析
数据特点 某个时刻的线程快照 某个时刻的堆快照 带时间信息的事件记录
性能开销 通常中等,取决于线程数量 较高,取决于堆大小 常规持续记录通常较低

三者可以配合使用:Thread Dump 定位线程等待位置,Heap Dump 分析对象引用,JFR 观察问题发生前后的运行过程。

5. 总结

JVM 问题排查主要解决 Java 服务运行过程中出现的 CPU 飙高、内存异常、GC 频繁以及线程阻塞等问题。整体思路是先通过监控确定异常现象,再根据问题类型使用 jstat、jcmd、线程堆栈、GC 日志、Heap Dump 或 JFR 定位具体原因。排查过程中主要关注三个方面:CPU 消耗在哪些线程上、内存为什么无法有效回收、业务线程为什么无法继续执行。同时需要区分正常内存增长与内存泄漏、Java 堆占用与进程整体内存占用。最终结合业务代码和运行时证据进行修复,并通过相同负载下的性能指标验证效果,形成完整的排查闭环。