序言
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 | 发现应用异常 |
这并不是固定的排查流程。实际工作中,应根据现象选择工具,并尽量先采集开销较低的信息。
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 | $ jps -l |
通过输出可以识别当前机器上的 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 | S0 S1 E O P YGC YGCT FGC FGCT GCT |
字段含义如下:
| 字段 | 含义 |
|---|---|
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 | jstack -l 31040 > /tmp/thread-1.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 | # 下载 Arthas 启动程序(示例) |
启动后,Arthas 会列出当前用户有权限访问的 Java 进程。选择目标进程对应的序号,即可 Attach 到该 JVM,并进入 Arthas 命令行。
需要注意,启动 Arthas 的用户通常需要与目标 Java 进程的运行用户一致,或者具备相应权限;否则可能出现 Attach 失败。如果目标 JVM 禁用了 Attach 机制,也无法通过常规方式连接。生产环境使用时,还应提前了解目标机器的安全策略、容器权限和网络限制。
查看 JVM 整体运行状态:dashboard
dashboard 可以展示目标 JVM 的实时状态,包括线程、内存、GC 和运行时信息。它适合用于问题排查的第一步,帮助我们快速判断异常更偏向 CPU、线程还是内存。
1 | # 查看实时 JVM 运行面板 |
面板中的线程信息通常包含线程名称、线程状态、CPU 使用率、采样期间 CPU 时间增量和累计 CPU 时间等字段。内存和 GC 区域则可以辅助观察堆、非堆及垃圾收集状态。
其中,CPU% 表示线程在采样间隔内消耗的 CPU 时间占采样间隔的比例。若某个业务线程的 CPU 使用率明显高于其他线程,可以继续使用 thread 命令查看其调用栈。
CPU 使用率过高:thread 和 profiler
当 Java 进程 CPU 使用率持续过高时,首先要确认是哪些线程消耗了 CPU。Arthas 的 thread 命令可以按线程 CPU 使用率排序,并直接输出最忙线程的堆栈。
1 | # 查看最忙的 3 个线程,并输出线程堆栈 |
thread -n 3 会根据采样得到的线程 CPU 使用率排序,展示最忙的 3 个线程及其堆栈。查看结果时,可以重点关注:
- 线程是否处于
RUNNABLE状态,并持续占用较高 CPU。 - 调用栈是否反复停留在某个业务方法或循环逻辑中。
- 是否是 GC、JIT 等 JVM 内部线程占用了较多 CPU。
- 是否存在频繁序列化、复杂计算、正则处理或高频重试等逻辑。
如果需要调整采样时间,可以使用 -i。例如 thread -n 3 -i 1000 按最近 1000 毫秒的线程 CPU 时间进行统计。采样本身也会产生一定开销;采样间隔过短时,诊断命令自身可能带来额外影响,因此应结合现场选择合适的间隔。
定位到可疑线程后,可以使用线程 ID 查看单个线程的堆栈:
1 | # 查看 ID 为 23 的线程堆栈,线程 ID 替换为实际输出值 |
这里的线程 ID 是 Arthas 输出的 Java 线程 ID,不应直接当作操作系统的原生线程 ID 使用。
如果还需要进一步定位具体哪些方法消耗 CPU,可以使用 Arthas 集成的 profiler 命令。它基于 async-profiler 采样并生成火焰图:
1 | # 开始 CPU 采样 |
命令执行完成后,Arthas 会输出火焰图文件的位置。可以通过 Arthas 提供的文件访问方式下载或打开 HTML 文件,再根据火焰图中占比较高的调用栈定位热点方法。
thread -n 更适合快速找到高 CPU 线程,profiler 更适合进一步分析方法级热点。两者关注点不同,可以配合使用。
线程阻塞与死锁排查:thread -b
当应用出现请求堆积、接口无响应或大量线程处于 BLOCKED 状态时,可以先使用 Arthas 查看线程状态:
1 | # 查看处于 BLOCKED 状态的线程 |
thread -b 会尝试找出持有锁、并且正在阻塞其他线程的线程。输出中如果出现类似下面的信息:
1 | - locked java.lang.Object@725be470 <---- but blocks 4 other threads! |
说明该线程持有对应的 synchronized 监视器锁,并且有其他线程正在等待这把锁。此时可以结合线程堆栈,检查持锁线程是否在临界区执行了耗时操作,或者是否由于异常逻辑长时间没有释放锁。
需要特别注意:thread -b 主要用于定位 synchronized 导致的锁阻塞,不等同于完整的死锁检测。 Arthas 官方文档说明,该功能目前不支持 java.util.concurrent.Lock。因此,如果怀疑存在真正的死锁,应进一步使用 JDK 的线程快照进行确认:
1 | # 在操作系统命令行中执行,输出额外锁信息 |
检查输出中是否出现 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 秒刷新一次 JVM 面板 |
memory 适合查看某一时刻的内存分布,dashboard 适合持续观察线程、内存和 GC 等指标。若发现堆内存持续增长,再考虑生成 Heap Dump 进行对象级分析。
生成 Heap Dump:heapdump
Arthas 也提供了 heapdump 命令,功能类似于 jmap -dump,可以将目标 JVM 的 Java 堆导出为 HPROF 文件:
1 | # 将堆快照导出到指定文件 |
将文件输出到 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 的主要功能可以归纳为四个方面:
- 方法调用分析:通过调用树、热点方法等视图,定位耗时较高的方法和调用链。
- 内存分配分析:观察对象分配情况、对象引用关系和垃圾回收情况,辅助排查内存泄漏。
- 线程与锁分析:查看线程状态、线程活动历史和锁竞争情况,定位线程阻塞问题。
- 高级子系统分析:结合 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 的实例数量或内部节点数量持续增长。此时不能直接认定存在内存泄漏,还需要进一步确认:
- 对象增长是否符合业务预期。
- 对象是否能够随着业务结束而释放。
- 是否有静态变量、缓存或长期存活对象持有这些引用。
- 对象数量增长是否导致堆内存持续增加。
发现可疑对象后,可以进一步使用 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 | 发现堆内存持续增长 |
第一步:找到可疑对象
先在 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 | OrderController.query() |
通过调用树,可以观察耗时主要集中在哪个方法或调用分支。对于层级较深的业务调用链,这种方式尤其有帮助。
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 | JDK 命令行工具 / Arthas:快速发现问题、采集现场 |
最后需要强调:诊断工具提供的是证据,而不是自动给出根因。比如,堆中某个类的实例数量很多,不一定意味着内存泄漏;线程处于 WAITING 状态,也不一定意味着线程异常。只有将 JVM 指标、对象引用、线程调用栈和实际业务逻辑结合起来,才能更准确地定位问题。
对于日常 Java 后端开发而言,建议至少熟悉 jps、jstat、jmap、jstack 和 jcmd 的基本使用方式,同时掌握 Arthas 的 dashboard、thread、memory 和 heapdump 等常用命令,并熟悉一种 Heap Dump 分析工具。遇到线上问题时,可以先通过 Arthas 快速观察目标 JVM 的状态,再根据问题类型使用 jstack、jcmd、JProfiler 或 MAT 深入分析。先采集证据、再定位原因,通常比一开始就盲目调整 JVM 参数更有效。
参考资料
- Oracle JDK 21 工具说明
- Oracle JDK 21 移除的工具和组件
- Oracle JDK 9 迁移指南:VisualVM 不再随 JDK 发布
- VisualVM 官方网站
- JProfiler 官方手册
- Arthas 官方文档:快速入门
文章信息
| 时间 | 说明 |
|---|---|
| 2026-07-19 | 初稿 |