JVM 诊断工具详解:从命令行到图形化分析

序言

  Java 应用在开发和运行过程中,难免会遇到各种 JVM 层面的问题。例如,应用运行一段时间后内存持续增长,最终出现 OutOfMemoryError;某个接口响应突然变慢,CPU 使用率却持续处于高位;线程池中的线程长期处于等待或阻塞状态;又或者 GC 频率异常,导致应用出现明显的停顿。

  这些问题通常不能只通过业务日志定位,还需要结合 JVM 的运行数据进行分析。JDK 提供了一系列命令行诊断工具,可以帮助我们查看虚拟机进程、内存、垃圾收集、线程以及运行参数等信息。除此之外,也可以使用 VisualVM、JProfiler、Arthas、Prometheus 等图形化工具,它们提供了更直观的监控视图和更深入的性能分析能力。

  本文按照 JVM 问题排查的实际思路,介绍常见的 JDK 命令行工具、Arthas 在线诊断工具和图形化诊断工具。除了使用 JProfiler 分析内存、CPU、线程和锁相关问题,文章还会结合 Arthas 的常用命令,说明如何在不重启应用的情况下连接目标 JVM,快速查看 CPU 占用、线程阻塞和堆内存状态。

  当然,部分工具和参数具有明显的 JDK 版本差异,本文会在相关位置说明 JDK 8 与较新 JDK 版本之间的区别,避免环境差异性导致处理出错。

JVM 诊断工具的基本概念

为什么需要 JVM 诊断工具

  JVM 运行时会产生大量状态信息,例如堆内存各区域的使用情况、垃圾收集次数和耗时、线程状态、类加载数量、JIT 编译情况等。

  当应用出现性能问题时,这些数据能够帮助我们缩小排查范围。例如:

  • CPU 使用率过高:需要确认是哪些线程占用了 CPU,以及线程正在执行哪些方法。
  • 内存持续增长或发生 OOM:需要分析堆中哪些对象数量异常、对象由谁引用,以及为什么无法被垃圾回收。
  • GC 频繁或停顿时间过长:需要观察各内存区域的使用情况、GC 发生频率和耗时。
  • 线程阻塞或死锁:需要查看线程状态、调用栈以及线程之间的锁依赖关系。可以使用 jstack 获取线程快照,也可以通过 Arthas 的 thread 命令在线查看线程和锁阻塞情况。
  • 应用启动或运行参数异常:需要确认 JVM 实际使用的参数和系统属性。

  仅依赖业务日志,通常很难完整回答这些问题。JVM 诊断工具的价值,就是把虚拟机内部的运行状态以可观察、可分析的形式呈现出来。

JVM 诊断的基本思路

  实际排查时,不建议一开始就生成大型 Heap Dump 或直接进行深度性能分析。更合理的方式是先通过轻量级信息确定问题方向,再根据需要采集更详细的数据。

  一个常见的排查过程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
发现应用异常
|
+-- CPU 使用率异常
| |
| +-- 使用 Arthas thread -n 查看最忙线程
| +-- 查看线程调用栈,必要时使用 profiler 生成 CPU 火焰图
| +-- 定位高 CPU 方法及其调用路径
|
+-- 内存使用异常
| |
| +-- 观察堆内存和 GC 情况
| +-- 判断是否持续增长
| +-- 生成 Heap Dump
| +-- 分析对象数量、大小和引用链
|
+-- GC 异常
| |
| +-- 观察 GC 次数、耗时和内存区域
| +-- 结合 GC 日志分析
| +-- 判断分配速率、晋升和回收是否异常
|
+-- 线程阻塞或死锁
|
+-- 使用 Arthas thread -b 排查 synchronized 锁阻塞
+-- 使用 thread --state BLOCKED 查看阻塞线程
+-- 必要时使用 jstack -l 获取完整线程快照并确认死锁

  这并不是固定的排查流程。实际工作中,应根据现象选择工具,并尽量先采集开销较低的信息。

JVM 诊断工具的分类

  从使用方式来看,常见工具可以分为命令行诊断工具和图形化诊断工具。其中,命令行工具又可以分为 JDK 自带工具和需要单独部署的第三方工具。

类型 代表工具 主要用途
JDK 命令行工具 jps、jstat、jinfo、jmap、jstack、jcmd 查看进程、运行参数、GC、内存和线程信息
第三方命令行工具 Arthas 在线连接运行中的 Java 进程,查看线程、内存、JVM 参数,并进行方法调用和性能诊断
图形化工具 VisualVM、JProfiler、Prometheus 监控运行状态、分析内存对象、方法调用、线程和锁

  这些工具可以结合不同场景进行使用:

  • JDK 自带命令适合快速采集基础信息
  • Arthas 更强调在线诊断,可以在连接目标 JVM 后,通过交互式命令查看线程、内存和运行状态
  • 图形化工具则更适合在本地或独立分析环境中持续观察和深入分析

  另外:不同 JDK 发行版和版本所包含的工具可能不同。本文提到的命令应以目标机器实际安装的 JDK 为准。

JDK 命令行诊断工具

  JDK 的 bin 目录中提供了多种诊断工具,例如 jps、jstat、jmap 和 jstack。除此之外,实际工作中还经常使用第三方在线诊断工具 Arthas。

  JDK 工具通常通过命令行直接读取目标 JVM 的信息;Arthas 则通过 Attach 机制连接到正在运行的 Java 进程,在交互式控制台中执行诊断命令。两类工具的定位不同,但在排查线上问题时可以互相配合。

jps:虚拟机进程状况工具

  jps 的全称是 JVM Process Status Tool,主要用于列出当前机器上能够识别到的 Java 虚拟机进程,并显示进程标识以及主类或启动信息。

  它的作用类似于操作系统中的 ps 命令,但更关注 Java 虚拟机进程。由于后续很多诊断命令都需要指定目标 JVM 的进程 ID,因此 jps 往往是排查 JVM 问题时首先使用的工具。

常用命令

命令 作用说明
jps 列出当前用户可见的 Java 虚拟机进程(常用)
jps -m 显示传递给主类的参数
jps -v 显示 JVM 启动时传递的参数
jps -l 显示完整包名或类名

  例如:

1
2
3
$ jps -l
3850 org.apache.catalina.startup.Bootstrap
4216 com.example.Application

  通过输出可以识别当前机器上的 Java 进程。确定目标进程后,就可以将对应的进程 ID 传递给 jstat、jmap、jstack 等工具。

LVMID 与 PID 的关系

  jps 输出的进程标识通常称为 LVMID(Local Virtual Machine Identifier)。对于本地 JVM 进程,在常见操作系统环境下,LVMID 通常与操作系统进程 ID(PID)一致。

  jps 主要解决“当前有哪些 Java 进程,以及我要排查的是哪一个进程”的问题。

jstat:虚拟机统计信息监视工具

  jstat 的全称是 JVM Statistics Monitoring Tool,用于监视 JVM 的类加载、垃圾收集、内存使用和 JIT 编译等运行时统计信息。

  jstat 适合持续观察 JVM 运行指标,尤其适合初步判断 GC 频率、GC 耗时和内存区域使用情况。

  jstat的一个重要特点是可以按照指定时间间隔持续输出数据。因此,在没有图形化界面的服务器上,可以通过 jstat 观察 GC 和内存使用情况,初步判断是否存在内存压力或 GC 异常。

命令格式

1
jstat [option] <vmid> [interval [count]]

  参数说明:

参数 含义
option 指定需要查看的统计信息,例如 -gc、-gcutil
vmid 目标 JVM 的标识;本地进程通常使用 PID
interval 采样间隔,默认单位为毫秒,也可以使用 s 指定秒
count 采样次数;省略时通常会持续输出,直到命令被终止

  例如,每隔 1000 毫秒采集一次进程 2764 的 GC 信息,共采集 50 次:

1
jstat -gc 2764 1000 50

常用监控选项

选项 说明
-gc 查看各内存区域容量、已使用空间以及 GC 次数和耗时(常用)
-gcutil 以百分比形式查看各内存区域的使用情况,以及 GC 次数和耗时(常用)
-class 查看类加载、卸载数量以及相关耗时
-gccapacity 查看各内存区域的容量信息
-gccause 在 GC 使用率信息基础上,额外显示最近一次 GC 的原因等信息
-gcnew 查看新生代相关统计信息
-gcnewcapacity 查看新生代各区域的容量信息
-gcold 查看老年代相关统计信息
-gcoldcapacity 查看老年代容量信息
-gcmetacapacity 查看元空间相关容量信息,适用于支持元空间的 JDK
-compiler 查看 JIT 编译器相关统计信息
-printcompilation 输出最近被 JIT 编译的方法信息

版本说明:JDK 8 及更早版本使用永久代(PermGen)相关统计项;JDK 8 起永久代被元空间(Metaspace)取代。因此,旧资料中的 -gcpermcapacity 等选项不能直接作为所有 JDK 版本的通用说明。具体可用选项应以当前 JDK 的 jstat -options 输出为准。

使用 jstat 观察 GC

  例如下面的命令代表每隔 1 秒输出一次 GC 使用率信息,共输出 10 次:

1
jstat -gcutil 2764 1000 10

  在 JDK 8 的典型输出中,可能看到类似下面的字段:

1
2
S0     S1      E       O       P      YGC    YGCT    FGC    FGCT    GCT
0.00 0.00 6.20 41.42 47.20 16 0.105 3 0.472 0.577

  字段含义如下:

字段 含义
S0、S1 Survivor 0、Survivor 1 的使用率
E Eden 区使用率
O Old 区使用率
P Perm 区使用率;在 JDK 8 的旧式输出语境中表示永久代
YGC Young GC 次数
YGCT Young GC 累计耗时(单位秒)
FGC Full GC 次数
FGCT Full GC 累计耗时(单位秒)
GCT GC 累计耗时(单位秒)

  从示例数据可以看出,Eden 区使用率为 6.20%,Old 区使用率为 41.42%,永久代使用率为 47.20%;程序运行以来发生了 16 次 Young GC 和 3 次 Full GC。

  需要注意,YGCT、FGCT 和 GCT 是累计耗时,不是单次 GC 的耗时。分析时应结合多次采样观察变化趋势,而不是只看某一次输出。

  另外,jstat 的列名和统计口径会受到 JDK 版本及垃圾收集器的影响。例如,使用 G1 等收集器时,输出字段可能与传统分代收集器的示例不同。不能脱离具体版本和收集器,仅凭某一个百分比就判断 JVM 是否存在故障。

jinfo:Java 配置信息工具

  jinfo 用于查看指定 Java 进程的 JVM 配置信息,包括虚拟机参数和系统属性。在部分支持动态调整的参数上,它还可以用于修改运行时参数。

  在排查问题时,启动脚本中配置的参数不一定等于 JVM 最终实际使用的参数。通过 jinfo 可以进一步确认目标进程的运行配置。

命令格式

1
jinfo [option] <pid>

  常用选项:

选项 说明
不指定选项 输出 Java 进程的配置信息
-flags 查看已设置的 JVM 参数(常用)
-flag <name> 查看指定 JVM 参数的当前值
-sysprops 输出系统属性,相当于查看 System.getProperties() 的内容
`-flag [+ -]` 对支持动态调整的布尔型 JVM 参数进行开启或关闭
-flag <name>=<value> 修改支持动态调整的 JVM 参数值

查看 JVM 参数

  例如,查看进程 3850 的最大堆内存参数:

1
jinfo -flag MaxHeapSize 3850

  也可以查看系统属性:

1
jinfo -sysprops 3850

动态调整 JVM 参数

  对于支持动态调整的参数,可以使用类似下面的方式修改:

1
jinfo -flag +PrintGC 3850

  但需要特别注意:并不是所有 JVM 参数都支持运行时修改。 对于不支持动态调整的参数,执行修改命令可能失败,或者无法达到预期效果。堆大小等很多关键参数通常需要在 JVM 启动时配置,不能简单地通过 jinfo 在运行过程中修改。

  此外,JDK 版本不同,部分参数名称和支持情况也可能发生变化。执行前应确认目标 JVM 的版本以及参数是否属于可管理参数。

jmap:Java 内存映像工具

  jmap 用于查看 Java 堆相关信息,并生成 Java 堆转储快照(Heap Dump)。Heap Dump 通常保存为 .hprof 文件,可用于后续分析对象数量、对象大小以及对象之间的引用关系。

  当应用出现内存持续增长、内存泄漏怀疑或 OutOfMemoryError 时,Heap Dump 是非常重要的分析材料。

常用选项

选项 说明
-dump 生成 Java 堆转储快照
-histo 输出堆中对象的统计信息,包括类名、实例数量和占用空间
-finalizerinfo 显示等待 Finalizer 线程处理的对象信息
-heap 输出 Java 堆的概要信息,例如堆配置和垃圾收集器信息;具体支持情况与平台和 JDK 版本有关
-F 在特定平台和版本中,当常规方式无法响应时尝试强制操作;并非所有环境都支持

生成 Heap Dump

  例如,将进程 117815 的堆转储到指定文件:

1
jmap -dump:format=b,file=/var/jvm/heap.dump 117815

  如果希望只导出当前被认为存活的对象,可以在支持的环境中使用 live 选项:

1
jmap -dump:live,format=b,file=/var/jvm/heap-live.hprof 117815

  live 方式可能触发 Full GC,因此采集过程对线上应用的影响需要特别关注。

查看对象统计信息

1
jmap -histo 117815

  如果只关注存活对象,可在支持的版本中使用:

1
jmap -histo:live 117815

  对象统计可以帮助我们快速发现实例数量或内存占用异常的类,但它不能直接说明对象为什么无法回收。要定位引用关系,通常还需要进一步分析 Heap Dump。

线上使用注意事项

  生成 Heap Dump 可能触发较长时间的停顿,并且会消耗额外的 CPU、磁盘 I/O 和磁盘空间。堆越大,生成和分析文件通常越耗时。

  因此,生产环境使用时需要考虑:

  • 尽可能选择业务低峰期执行
  • 评估当前服务的负载和可用磁盘空间
  • 集群部署时,可以结合业务情况将待采集节点从流量中摘除
  • 若 JVM 已经处于严重内存压力下,应评估采集操作是否可能进一步影响服务
  • 明确 Dump 文件可能包含用户、业务数据等敏感信息,应限制文件访问权限并妥善传输和保存

  除了 jmap 和 jcmd,也可以在已连接目标 JVM 的 Arthas 控制台中使用 heapdump 命令导出堆快照,后文会介绍其用法。对于较新的 JDK,也可以使用 jcmd 的诊断命令生成 Heap Dump,例如:

1
jcmd 117815 GC.heap_dump /var/jvm/heap.hprof

  jmap 可以帮助我们生成和查看堆快照,但 Heap Dump 只是分析材料。真正定位内存问题,还需要借助分析工具检查对象分布和引用链。

jhat:虚拟机堆转储快照分析工具(了解)

  jhat(JVM Heap Analysis Tool)是早期 JDK 提供的 Heap Dump 分析工具,通常与 jmap 配合使用。

  它可以解析 Heap Dump,并启动一个简单的 HTTP 服务,将分析结果通过浏览器展示出来。

基本使用方式

1
jhat /var/jvm/heap.dump

  启动后,根据命令输出的地址在浏览器中访问分析页面。

工具局限性

  jhat 的功能相对基础,分析能力和交互体验有限。实际工作中,更常见的做法是将 Heap Dump 文件复制到独立分析机器,再使用 VisualVM、Eclipse Memory Analyzer(MAT)或 JProfiler 等工具进行分析。

  还需要注意版本问题:jhat 属于较早期的诊断工具,已经从后续 JDK 版本中移除,不能假设在当前 JDK 环境中一定存在。JDK 21 的迁移文档也将其列为已移除工具。

  jhat 主要用于了解早期 Heap Dump 分析方式。现代 Java 项目通常优先使用 MAT、VisualVM 或 JProfiler 等工具。

jstack:Java 线程堆栈跟踪工具(了解)

  jstack 用于生成指定 JVM 进程在某一时刻的线程快照(Thread Dump)。线程快照包含 JVM 中线程的状态、调用栈以及部分锁信息。

  当应用出现响应缓慢、线程阻塞、死锁或 CPU 使用率异常时,Thread Dump 能够帮助我们了解线程当时正在执行什么,或者正在等待什么资源。

命令格式

1
jstack [options] <pid>

  常见选项包括:

选项 说明
-l 输出额外的锁相关信息
-e 输出更多线程相关信息,具体支持情况以当前 JDK 为准

生成线程快照

  例如,将进程 31040 的线程快照保存到文件:

1
jstack -l 31040 > /tmp/31040-thread-dump.txt

  为了分析线程状态变化,可以在短时间内连续采集多份 Thread Dump:

1
2
3
4
5
jstack -l 31040 > /tmp/thread-1.txt
sleep 5
jstack -l 31040 > /tmp/thread-2.txt
sleep 5
jstack -l 31040 > /tmp/thread-3.txt

  将多份快照进行对比,可以观察线程是否长期停留在同一个调用位置,或者是否存在持续等待、锁竞争等情况。

常见排查方向

  • 死锁:检查线程快照中是否出现 JVM 检测到的 Java-level deadlock 信息,以及线程之间的锁等待关系。
  • 线程阻塞:观察线程是否处于 BLOCKED 状态,以及正在等待哪个监视器锁。
  • 线程等待:观察 WAITING、TIMED_WAITING 状态,结合调用栈判断线程是在等待任务、等待锁,还是等待外部资源。
  • 高 CPU 线程:结合操作系统线程信息,将高 CPU 的线程映射到 Thread Dump 中,再分析其调用栈是否存在死循环或高频计算。

  需要注意,WAITING 或 TIMED_WAITING 并不一定代表异常。线程池中的空闲线程、定时任务线程等都可能处于等待状态,必须结合业务场景和调用栈判断。

  jstack 主要解决线程层面的问题。分析时不能只看线程状态,还要结合调用栈、锁信息以及多次快照的变化。对于需要快速在线查看线程状态的场景,也可以使用 Arthas 的 thread 命令;如果要判断是否存在完整的 Java 级别死锁,仍应结合 jstack -l 等线程快照进行确认。

Arthas:Java 应用在线诊断工具

  Arthas 是一款开源的 Java 诊断工具,可以在应用运行过程中通过 Attach 机制连接到目标 JVM,并通过交互式命令查看运行状态、线程堆栈、内存信息和 JVM 参数。它不属于 JDK 自带工具,需要单独下载和启动。

  与 jps、jstat、jmap 等单项命令相比,Arthas 将多种诊断能力集中在一个控制台中。遇到线上 CPU 使用率过高、线程阻塞或堆内存异常时,可以先使用 Arthas 快速查看现场,再决定是否需要生成 Thread Dump、Heap Dump,或者使用 JProfiler、MAT 进一步分析。

启动并连接目标 Java 进程

  通常可以通过 arthas-boot.jar 启动 Arthas:

1
2
3
4
5
# 下载 Arthas 启动程序(示例)
curl -O https://arthas.aliyun.com/arthas-boot.jar

# 启动 Arthas,并选择需要诊断的 Java 进程
java -jar arthas-boot.jar

  启动后,Arthas 会列出当前用户有权限访问的 Java 进程。选择目标进程对应的序号,即可 Attach 到该 JVM,并进入 Arthas 命令行。

  需要注意,启动 Arthas 的用户通常需要与目标 Java 进程的运行用户一致,或者具备相应权限;否则可能出现 Attach 失败。如果目标 JVM 禁用了 Attach 机制,也无法通过常规方式连接。生产环境使用时,还应提前了解目标机器的安全策略、容器权限和网络限制。

查看 JVM 整体运行状态:dashboard

  dashboard 可以展示目标 JVM 的实时状态,包括线程、内存、GC 和运行时信息。它适合用于问题排查的第一步,帮助我们快速判断异常更偏向 CPU、线程还是内存。

1
2
3
4
5
# 查看实时 JVM 运行面板
dashboard

# 每隔 2 秒刷新一次,共刷新 10 次
dashboard -i 2000 -n 10

  面板中的线程信息通常包含线程名称、线程状态、CPU 使用率、采样期间 CPU 时间增量和累计 CPU 时间等字段。内存和 GC 区域则可以辅助观察堆、非堆及垃圾收集状态。

  其中,CPU% 表示线程在采样间隔内消耗的 CPU 时间占采样间隔的比例。若某个业务线程的 CPU 使用率明显高于其他线程,可以继续使用 thread 命令查看其调用栈。

CPU 使用率过高:thread 和 profiler

  当 Java 进程 CPU 使用率持续过高时,首先要确认是哪些线程消耗了 CPU。Arthas 的 thread 命令可以按线程 CPU 使用率排序,并直接输出最忙线程的堆栈。

1
2
3
4
5
# 查看最忙的 3 个线程,并输出线程堆栈
thread -n 3

# 按最近 1 秒的 CPU 时间进行统计,列出最忙的 3 个线程
thread -n 3 -i 1000

  thread -n 3 会根据采样得到的线程 CPU 使用率排序,展示最忙的 3 个线程及其堆栈。查看结果时,可以重点关注:

  • 线程是否处于 RUNNABLE 状态,并持续占用较高 CPU。
  • 调用栈是否反复停留在某个业务方法或循环逻辑中。
  • 是否是 GC、JIT 等 JVM 内部线程占用了较多 CPU。
  • 是否存在频繁序列化、复杂计算、正则处理或高频重试等逻辑。

  如果需要调整采样时间,可以使用 -i。例如 thread -n 3 -i 1000 按最近 1000 毫秒的线程 CPU 时间进行统计。采样本身也会产生一定开销;采样间隔过短时,诊断命令自身可能带来额外影响,因此应结合现场选择合适的间隔。

  定位到可疑线程后,可以使用线程 ID 查看单个线程的堆栈:

1
2
# 查看 ID 为 23 的线程堆栈,线程 ID 替换为实际输出值
thread 23

  这里的线程 ID 是 Arthas 输出的 Java 线程 ID,不应直接当作操作系统的原生线程 ID 使用。

  如果还需要进一步定位具体哪些方法消耗 CPU,可以使用 Arthas 集成的 profiler 命令。它基于 async-profiler 采样并生成火焰图:

1
2
3
4
5
6
7
8
# 开始 CPU 采样
profiler start --event cpu

# 查看采样状态
profiler status

# 停止采样并生成 HTML 火焰图
profiler stop --format html

  命令执行完成后,Arthas 会输出火焰图文件的位置。可以通过 Arthas 提供的文件访问方式下载或打开 HTML 文件,再根据火焰图中占比较高的调用栈定位热点方法。

  thread -n 更适合快速找到高 CPU 线程,profiler 更适合进一步分析方法级热点。两者关注点不同,可以配合使用。

线程阻塞与死锁排查:thread -b

  当应用出现请求堆积、接口无响应或大量线程处于 BLOCKED 状态时,可以先使用 Arthas 查看线程状态:

1
2
3
4
5
# 查看处于 BLOCKED 状态的线程
thread --state BLOCKED

# 查看当前阻塞其他线程的线程
thread -b

  thread -b 会尝试找出持有锁、并且正在阻塞其他线程的线程。输出中如果出现类似下面的信息:

1
- locked java.lang.Object@725be470 <---- but blocks 4 other threads!

  说明该线程持有对应的 synchronized 监视器锁,并且有其他线程正在等待这把锁。此时可以结合线程堆栈,检查持锁线程是否在临界区执行了耗时操作,或者是否由于异常逻辑长时间没有释放锁。

  需要特别注意:thread -b 主要用于定位 synchronized 导致的锁阻塞,不等同于完整的死锁检测。 Arthas 官方文档说明,该功能目前不支持 java.util.concurrent.Lock。因此,如果怀疑存在真正的死锁,应进一步使用 JDK 的线程快照进行确认:

1
2
# 在操作系统命令行中执行,输出额外锁信息
jstack -l <pid> > /tmp/thread-dump.txt

  检查输出中是否出现 Found one Java-level deadlock,并分析涉及线程的 waiting to lock、locked 等信息。thread --state BLOCKED 可以帮助筛选阻塞线程,thread -b 可以辅助找到持有 synchronized 锁并阻塞其他线程的线程,而 jstack -l 更适合结合完整锁信息确认 Java 级别死锁。

查看堆内存:memory 和 dashboard

  如果怀疑堆内存使用过高,可以先使用 memory 查看 JVM 内存各区域的使用情况:

1
memory

  该命令会展示堆内存、非堆内存以及不同内存池的 used、total、max 和 usage 等信息。常见区域包括:

区域 含义
heap Java 堆内存整体使用情况
g1_eden_space G1 收集器下的 Eden 区
g1_old_gen G1 收集器下的老年代
g1_survivor_space G1 收集器下的 Survivor 区
nonheap 非堆内存整体使用情况
metaspace 元空间,用于存储类元数据等信息
codeheap_* JIT 编译相关的代码缓存区域
direct 通过 Buffer 等机制使用的直接内存统计信息

  具体内存池名称会根据 JDK 版本和垃圾收集器不同而变化;某些区域的 max 可能显示为 -1,表示该内存池没有提供固定的最大值。

  如果希望连续观察内存和 GC 的变化,可以使用:

1
2
# 每隔 2 秒刷新一次 JVM 面板
dashboard -i 2000

  memory 适合查看某一时刻的内存分布,dashboard 适合持续观察线程、内存和 GC 等指标。若发现堆内存持续增长,再考虑生成 Heap Dump 进行对象级分析。

生成 Heap Dump:heapdump

  Arthas 也提供了 heapdump 命令,功能类似于 jmap -dump,可以将目标 JVM 的 Java 堆导出为 HPROF 文件:

1
2
3
4
5
# 将堆快照导出到指定文件
heapdump arthas-output/heap.hprof

# 只导出存活对象
heapdump --live arthas-output/heap-live.hprof

  将文件输出到 arthas-output 目录时,可以通过 Arthas 提供的文件访问入口下载到本地,再使用 JProfiler 或 Eclipse MAT 分析对象数量、内存占用和 GC Roots 引用链。--live 会只导出存活对象,采集过程可能触发垃圾收集并造成停顿,生产环境执行前应评估影响。

  Heap Dump 往往体积较大,导出前应确认磁盘空间是否充足,并注意文件中可能包含业务对象和敏感数据。不要在不了解影响的情况下频繁执行堆转储。

Arthas 常用 JVM 诊断命令小结

排查目的 Arthas 命令 说明
查看 JVM 实时状态 dashboard 查看线程、内存、GC 等整体信息
查看最忙线程 thread -n 3 按 CPU 使用率排序并输出线程堆栈
调整 CPU 采样间隔 thread -n 3 -i 1000 按指定采样间隔统计最忙线程
查看单个线程 thread <id> 查看指定线程的调用栈
查看阻塞线程 thread --state BLOCKED 筛选处于 BLOCKED 状态的线程
查找锁阻塞源 thread -b 查找持有 synchronized 锁并阻塞其他线程的线程
查看内存区域 memory 查看堆、非堆和内存池使用情况
生成 CPU 火焰图 profiler start / profiler stop 采样并分析 CPU 热点调用栈
生成堆快照 heapdump <file> 导出 Heap Dump 文件

  Arthas 适合线上快速诊断,尤其是通过 thread -n 定位高 CPU 线程、通过 thread -b 排查 synchronized 锁阻塞、通过 memory 和 dashboard 观察内存状态,以及通过 heapdump 导出堆快照。它不能替代所有 JDK 工具:例如,确认 Java 级别死锁时仍建议结合 jstack -l;分析对象引用链时,则需要进一步使用 JProfiler 或 MAT。

图形化综合诊断工具

  前面介绍的 JDK 命令行工具和 Arthas,能够帮助我们在服务器上快速采集 JVM 运行信息。其中,Arthas 还提供线程 CPU 排序、实时面板、堆内存查看和 Heap Dump 导出等在线诊断能力。不过,命令行输出仍以文本为主,在观察长期趋势、分析复杂对象引用关系和方法调用链时,图形化工具通常更加直观。

  命令行工具在深入分析时也存在一些局限:

  • 输出主要以文本形式呈现,不容易直观地观察长期变化趋势。
  • 部分命令只能提供统计信息,难以直接展示方法调用关系和对象引用链。
  • 对复杂问题而言,需要手动组合多种命令,并对结果进行交叉分析。
  • 方法级性能分析、内存分配热点等问题,通常需要更专业的分析工具。

  图形化诊断工具可以将 JVM 运行数据以图表、调用树、引用关系图等方式展示,降低分析复杂度。

VisualVM

  VisualVM 是一款用于监控和分析 Java 应用的图形化工具,可以查看目标 JVM 的 CPU、内存、线程、类加载等运行信息,也支持对 Heap Dump 进行分析。

  常见功能包括:

  • 监控(Monitor):查看堆内存、元空间、线程数量和类加载等运行指标。
  • CPU 分析:观察方法执行情况,辅助定位性能热点。
  • 内存分析:查看对象数量、对象大小和内存分配情况。
  • 线程分析:查看线程状态和线程转储。
  • Heap Dump 分析:查看堆中对象及其引用关系。

  VisualVM 适合日常 JVM 运行状态观察和基础性能分析。对于复杂的内存泄漏问题,也可以结合专业 Heap Dump 分析工具进一步定位。

版本说明

  VisualVM 曾随 Oracle JDK 6、JDK 7 和 JDK 8 一起提供。从 JDK 9 开始,它不再随 Oracle JDK 一起发布,需要单独获取和安装。当前可以使用独立发行的 VisualVM,而不是依赖目标 JDK 的 bin 目录中一定存在 jvisualvm。

JProfiler

  JProfiler 是一款专业的 Java 应用性能诊断和分析工具。相比只观察 JVM 的基础指标,它更强调对方法调用、对象分配、线程状态和锁竞争等问题进行深入分析。

  JProfiler 的主要功能可以归纳为四个方面:

  1. 方法调用分析:通过调用树、热点方法等视图,定位耗时较高的方法和调用链。
  2. 内存分配分析:观察对象分配情况、对象引用关系和垃圾回收情况,辅助排查内存泄漏。
  3. 线程与锁分析:查看线程状态、线程活动历史和锁竞争情况,定位线程阻塞问题。
  4. 高级子系统分析:结合 JDBC、JMS 等子系统的调用信息,分析更贴近业务语义的性能问题。

  接下来重点介绍 JProfiler 中与内存、CPU、线程和锁相关的常用视图。

JProfiler 核心功能与实战

Heap Dump 文件分析

  Heap Dump 是某一时刻 Java 堆内存的快照,记录了堆中的对象、对象之间的引用关系以及相关类信息。通过分析 Heap Dump,可以回答以下问题:

  • 当前堆中哪些类的实例数量最多?
  • 哪些对象占用了大量内存?
  • 某个对象为什么没有被垃圾回收?
  • 哪些集合或业务对象持有了大量引用?
  • 是否存在对象被意外长期保留的情况?

  通常可以先通过 jmap 或 jcmd 生成 Heap Dump,再将文件传输到本地分析机器,使用 JProfiler 打开。

  例如:

1
jcmd <pid> GC.heap_dump /tmp/heap.hprof

  Heap Dump 文件可能非常大,打开时需要解析文件并建立索引,因此首次加载可能需要较长时间,也会占用较多内存和 CPU。分析时应确保本地机器有足够的资源,不建议在生产业务机器上直接进行大型 Dump 的深度分析。

文件扩展名本身并不能决定文件内容。不要仅通过修改后缀名来判断文件格式;应确认文件确实是兼容的 HPROF Heap Dump,再使用分析工具打开。

Memory Views:内存视图

  Memory Views 主要用于观察 JVM 中对象的分配和存活情况。它可以帮助我们发现哪些类的实例数量较多、哪些对象分配频繁,以及对象数量是否随时间持续增长。

  常见视图包括:

视图 主要用途
All Objects 查看当前堆中各类对象的数量和大小等统计信息
Recorded Objects 查看记录期间被跟踪的对象信息
Allocation Call Tree 按调用关系展示对象分配路径
Allocation Hot Spots 按方法、类等维度展示对象分配热点
Class Tracker 持续跟踪指定类或包的实例数量变化

  其中,All Objects 更关注当前对象存量,Allocation Hot Spots 更关注对象从哪里分配出来。两者解决的问题不同:

  • 如果想知道“当前哪些对象占用内存较多”,可以先看对象统计。
  • 如果想知道“哪些代码路径持续创建对象”,可以看分配调用树和分配热点。
  • 如果想知道“某个类的实例数量是否一直增长”,可以使用类跟踪视图观察变化趋势。

通过对象数量变化发现异常

  例如,某个服务运行一段时间后,发现 ConcurrentHashMap 的实例数量或内部节点数量持续增长。此时不能直接认定存在内存泄漏,还需要进一步确认:

  1. 对象增长是否符合业务预期。
  2. 对象是否能够随着业务结束而释放。
  3. 是否有静态变量、缓存或长期存活对象持有这些引用。
  4. 对象数量增长是否导致堆内存持续增加。

  发现可疑对象后,可以进一步使用 Heap Walker 分析具体实例和引用链。

Heap Walker:堆遍历器

  Heap Walker 是 JProfiler 中用于深入分析堆对象的功能。它可以基于堆快照查看对象实例、对象分配信息以及对象之间的引用关系。

  在实际排查中,可以先在 Memory Views 中发现增长异常的类,再将相关对象带入 Heap Walker 进一步分析。

  例如,发现 ConcurrentHashMap 的对象数量异常,可以在对应视图中选中该类或对象,通过右键菜单进入 Heap Walker。不同 JProfiler 版本的菜单名称和操作入口可能略有差异,应以实际界面为准。

  Heap Walker 常见的分析视图包括:

视图 主要用途
Classes 查看类及其实例
Allocations 查看对象的分配信息
References 查看对象之间的引用关系
Time 按时间维度观察记录对象的相关信息
Inspections 根据内置检查规则筛选和分析对象

Classes:类视图

  Classes 视图用于查看堆中的类以及对应实例。选中某个类后,可以进一步查看它的对象实例,并缩小后续分析范围。

  例如,发现某个业务缓存类的实例数量异常,可以进入该类对应的实例列表,检查具体对象。

Allocations:分配视图

  Allocations 视图用于分析对象的分配来源,可以结合分配树和热点信息,定位对象由哪些方法创建。

  它更适合回答“对象是在哪里创建的”,而不是直接回答“对象为什么没有被回收”。

References:引用视图

  References 是排查内存泄漏时非常重要的视图。Java 对象只要仍然能够从 GC Roots 到达,就可能继续存活。因此,分析对象的引用关系,有助于判断对象为什么没有被回收。

  References 通常可以从两个方向观察:

  • Incoming References:查看当前对象被哪些对象引用,也就是“谁持有我”。
  • Outgoing References:查看当前对象引用了哪些其他对象,也就是“我持有谁”。

  通过引用关系,可以逐步定位对象被长期保留的原因。

Time:时间视图

  Time 视图用于从时间维度观察记录对象的相关信息,帮助分析对象在记录期间的变化情况。具体展示内容和可用操作可能因 JProfiler 版本而异。

Inspections:检查视图

  Inspections 提供一组对象检查和筛选能力,可以根据特定条件从当前对象集合中筛选出值得进一步分析的对象。可以把它理解为一种辅助分析和缩小范围的工具。

内存泄漏排查实战

  内存泄漏并不一定意味着对象完全无法访问,而是指对象已经不再需要,却仍然被某些引用链持有,导致 GC 无法回收。

  例如,某个应用使用静态 Map 保存业务对象,但业务结束后没有移除对应记录。随着请求不断增加,Map 中的对象持续累积,最终可能造成堆内存不断增长。

  可以按照下面的思路排查:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
发现堆内存持续增长
|
v
通过 Memory Views 观察对象分布
|
v
定位实例数量或占用空间异常的类
|
v
进入 Heap Walker 查看具体对象实例
|
v
通过 References 分析对象引用关系
|
v
查找从 GC Roots 到目标对象的引用路径
|
v
定位长期持有对象的集合、缓存或业务对象
|
v
结合代码确认是否存在不合理的对象生命周期

第一步:找到可疑对象

  先在 Memory Views 中观察对象数量和内存占用情况。如果某个类的实例数量持续增加,且与业务请求量不匹配,就可以将它作为候选对象。

第二步:查看对象实例

  进入 Heap Walker,选择对应类并查看具体实例。通过实例列表,可以进一步确认是否存在大量相似对象,以及这些对象是否属于同一业务类型。

第三步:分析引用关系

  选中可疑对象,切换到 References 视图,观察 Incoming References 和 Outgoing References。

  其中,Incoming References 更值得关注,因为它可以帮助我们找到“是谁还在持有这个对象”。

第四步:查找 GC Roots 引用链

  通过类似 Show Paths To GC Root 的功能,可以查找从 GC Roots 到目标对象的引用路径。

  如果对象能够通过静态变量、线程、活动栈或其他 GC Roots 关联对象继续被访问,那么它就可能无法被回收。

  需要注意,找到引用路径并不意味着一定存在内存泄漏。还要结合对象的业务生命周期判断:如果对象本来就应该长期存在,那么这条引用链可能是合理的;如果对象早已失去业务价值,却仍然被长期持有,才需要进一步检查代码。

CPU Views:CPU 性能分析

  CPU Views 主要用于分析方法调用和执行耗时,帮助定位应用中的性能热点。

  常见视图包括:

视图 主要用途
Call Tree 以树状结构展示方法调用关系和耗时分布
Hot Spots 展示消耗时间较多的方法或调用热点
Call Graph 以图形方式展示方法之间的调用关系
Method Statistics 展示方法调用次数和时间等统计信息

Call Tree:调用树

  Call Tree 从调用关系出发,以树状结构展示方法的调用路径和性能数据。

  例如,一个接口的调用链可能如下:

1
2
3
4
5
6
7
8
9
10
11
OrderController.query()
|
+-- OrderService.query()
|
+-- OrderMapper.selectList()
|
+-- UserService.getUser()
| |
| +-- RedisClient.get()
|
+-- PermissionService.check()

  通过调用树,可以观察耗时主要集中在哪个方法或调用分支。对于层级较深的业务调用链,这种方式尤其有帮助。

Hot Spots:热点方法

  Hot Spots 用于找出消耗时间较多的方法。它可以帮助我们快速定位性能分析的重点。

  例如,某个接口整体耗时较高,通过热点视图发现大量时间消耗在 JSON 序列化、复杂计算或某个第三方调用上,就可以进一步检查对应代码。

  需要注意,热点方法不一定就是问题根因。还需要结合调用次数、单次耗时、调用上下文和采样方式进行判断。

Call Graph:调用关系图

  Call Graph 以图形方式展示方法之间的调用关系。相比树状结构,它更适合观察方法之间复杂的关联。

Method Statistics:方法统计

  Method Statistics 用于查看一段记录期间的方法调用统计信息,例如调用次数和执行时间等。可以用于对比不同方法的执行开销。

Thread Views:线程分析

  Thread Views 用于观察线程的状态、活动过程和调用堆栈,帮助定位线程阻塞、线程等待以及线程运行异常等问题。

  常见视图包括:

视图 主要用途
Thread History 展示线程活动及状态随时间变化的历史
Thread Monitor 查看当前线程及其活动状态
Thread Dumps 查看线程转储和调用堆栈

Thread History:线程历史

  Thread History 通过时间轴展示线程活动和状态变化,可以帮助我们观察线程在一段时间内是否频繁切换状态,是否存在长时间阻塞或等待。

Thread Monitor:线程监控

  Thread Monitor 用于查看当前线程及其状态。通过线程状态,可以初步判断线程是在执行、等待还是阻塞。

Thread Dumps:线程转储

  Thread Dumps 展示线程的调用堆栈,适合进一步分析线程当时正在执行什么操作,以及线程为什么停留在某个位置。

  线程分析时,需要将线程状态与调用栈结合起来。例如,线程处于 BLOCKED 状态时,应继续查看它等待的锁以及持有锁的线程,而不是仅凭状态就判断问题原因。

Monitor Views:锁与监视器分析

  Monitor Views 主要用于分析线程之间的锁竞争、监视器等待和阻塞情况。

  常见视图包括:

视图 主要用途
Current Locking Graph 展示当前 JVM 中的锁竞争和锁依赖关系
Current Monitors 查看当前监视器相关的等待或阻塞情况
Locking History Graph 展示记录期间锁竞争关系的变化
Monitor History 展示监视器等待和阻塞的历史情况
Monitor Usage Statistics 统计监视器使用情况

  通过这些视图,可以进一步判断:

  • 是否存在多个线程竞争同一把锁。
  • 是否有线程持有锁的时间过长。
  • 是否存在锁等待链条。
  • 是否出现死锁或潜在的死锁风险。

  例如,某个业务方法使用 synchronized 保护共享资源,如果临界区内又执行了耗时的数据库操作,那么其他线程就可能长时间等待这把锁。通过锁监控视图,可以辅助定位这种锁竞争问题。

  需要注意,锁竞争、线程阻塞和死锁是不同的问题。锁竞争不一定意味着死锁;线程处于等待状态也不一定是异常。应结合线程转储、锁依赖关系和业务代码进一步判断。

VM Telemetry Views:JVM 运行状态监控

  VM Telemetry Views 用于从时间维度观察 JVM 的运行指标。它更关注一段时间内各项指标的变化趋势,而不是某一时刻的静态状态。

  原文涉及的常见视图包括:

视图 主要用途
Memory 展示堆内存使用情况及变化趋势
Recorded Objects 展示记录对象数量等变化情况
Recorded Throughput 展示对象创建和释放等相关活动的变化情况
GC Activity 展示垃圾回收活动
Classes 展示已加载类数量的变化
Threads 展示线程数量及变化
CPU Load 展示 CPU 负载变化

  这些视图适合用来观察 JVM 是否存在持续性异常。例如:

  • 堆内存使用量是否持续上升。
  • GC 活动是否突然变得频繁。
  • 线程数量是否不断增长。
  • CPU 负载是否在某个时间段明显升高。
  • 类加载数量是否出现异常变化。

  需要说明,JProfiler 的视图名称、布局和统计口径可能随版本变化,实际使用时应以所安装版本的界面和官方手册为准。

JVM 诊断工具的选择与落地

  JVM 诊断工具并不是越多越好,关键是根据问题现象选择合适的工具。命令行工具适合快速采集基础信息,图形化工具适合观察趋势和深入分析。

  下面将常见问题与工具进行对应:

排查场景 优先使用的工具 主要分析内容
确认 Java 进程 jps 进程标识、主类和启动信息
查看 GC 和内存变化 jstat、Arthas dashboard GC 次数、耗时、内存区域使用率和趋势
确认 JVM 参数 jinfo、Arthas vmoption / jvm JVM 参数和运行时信息
查看对象统计 jmap -histo 类实例数量和对象占用空间
查看当前堆内存 Arthas memory 堆、非堆和内存池的使用情况
导出 Heap Dump jmap、jcmd、Arthas heapdump 生成堆快照,供后续分析
分析内存泄漏 jmap / jcmd / Arthas + JProfiler / MAT Heap Dump、对象实例和 GC Roots 引用链
排查线程阻塞 Arthas thread -b、thread --state BLOCKED 定位 synchronized 锁阻塞及 BLOCKED 线程
确认 Java 级别死锁 jstack -l 线程状态、调用栈和完整锁依赖关系
定位高 CPU 线程 Arthas thread -n 3 按 CPU 使用率排序并查看线程堆栈
定位 CPU 方法热点 Arthas profiler、JProfiler 火焰图、热点方法和调用耗时
观察 JVM 长期运行趋势 VisualVM / JProfiler / Arthas dashboard 内存、GC、线程和 CPU 变化

  可以把整个诊断过程归纳为:

1
2
3
4
5
6
7
8
9
10
JDK 命令行工具 / Arthas:快速发现问题、采集现场
|
v
图形化分析工具:观察趋势、深入分析对象和调用链
|
v
结合代码和业务:确认根因、制定修复方案
|
v
修复后持续观察:验证问题是否真正解决

  最后需要强调:诊断工具提供的是证据,而不是自动给出根因。比如,堆中某个类的实例数量很多,不一定意味着内存泄漏;线程处于 WAITING 状态,也不一定意味着线程异常。只有将 JVM 指标、对象引用、线程调用栈和实际业务逻辑结合起来,才能更准确地定位问题。

  对于日常 Java 后端开发而言,建议至少熟悉 jps、jstat、jmap、jstack 和 jcmd 的基本使用方式,同时掌握 Arthas 的 dashboard、thread、memory 和 heapdump 等常用命令,并熟悉一种 Heap Dump 分析工具。遇到线上问题时,可以先通过 Arthas 快速观察目标 JVM 的状态,再根据问题类型使用 jstack、jcmd、JProfiler 或 MAT 深入分析。先采集证据、再定位原因,通常比一开始就盲目调整 JVM 参数更有效。

参考资料

文章信息

时间 说明
2026-07-19 初稿
0%