VisualVM这个工具,很多人第一次见到它是在JDK安装目录的bin文件夹里,双击打开后看到一堆本地Java进程,却不知道接下来能做什么。我用它排查过不少诡异问题——CPU莫名其妙飙高、堆内存只涨不跌、线程池集体卡死,VisualVM就是那个能让你“看见”JVM内部状态的工具。这篇文章我不会讲太多理论,就按实际排查问题的顺序,把启动、本地监控、堆Dump、远程连接、常见坑一条龙讲透,让你看完就能上手用起来。
1. VisualVM到底是什么:先记住这四个核心用途
在开始操作之前,得先搞清楚VisualVM能解决什么问题。它不是压测工具,不是日志分析工具,也不负责帮你调优参数,它更像一辆“JVM体检车”,把Java进程运行时最关键的几项健康指标拉到你面前,让你用肉眼就能看到异常。
1.1 一张表看懂它能监控哪些指标
很多刚接触的人被一堆面板吓住,其实核心就四个维度:CPU、内存、线程、类加载。我整理成一张表,你对照着看就知道每项指标背后对应什么问题。
| 监控维度 | 你能看到什么 | 主要用来排查什么 |
|---|---|---|
| CPU | 每个线程的CPU占用、热点方法调用频率 | 线程死循环、频繁GC、CPU飙高 |
| 堆内存 | Eden/Survivor/Old区的实时占用、GC次数与耗时 | 内存泄漏、Full GC频繁、对象分配压力 |
| 元空间 | Metaspace区的加载量 | 动态生成类过多导致的OOM |
| 线程 | 线程状态、线程栈、死锁检测 | 线程阻塞、锁竞争、线程泄漏 |
| 类加载 | 已加载类数量、每个ClassLoader加载的类 | 动态代理/反射异常、类卸载失败 |
用生活类比的话,CPU面板相当于看一个人时刻在忙什么,线程面板是看他在排队还是在干活,堆内存面板是看仓库里堆了多少货、货还剩多少地方能放,类加载面板是看仓库里办了多少张出入证。
1.2 它和JConsole、Arthas这些工具有什么区别
很多人会问,有JConsole不就行了吗?为什么还要单独学VisualVM?我实际用下来的感受是:
- JConsole:能做基础监控,但功能太薄,想导出堆Dump、分析线程栈、做CPU采样都捉襟见肘,更像一个只能看体温的温度计。
- VisualVM:相当于一台便携B超机,除了基础监控,还能做堆Dump、线程转储、CPU采样、插件扩展,覆盖面广,操作又是图形化点一点就行,学习成本低。
- Arthas:命令行在线诊断神器,能反编译、能直接改运行中的逻辑,功能更猛,但上手门槛高,适合已经知道问题大概方向之后去做精准定位。
所以我的建议是:日常巡检、初步排查、快速定位“是不是JVM层面的问题”,用VisualVM最合适;等发现问题细节需要在线干预时,再上Arthas也不迟。这篇文章只讲VisualVM,因为它足以覆盖80%的日常诊断场景。
2. 启动与连接:5分钟跑通本地进程监控
VisualVM的获取和使用门槛很低,但有几个细节容易被新手忽略,我一步步说清楚。
2.1 VisualVM从哪里获取和启动
最省事的方式是直接用JDK自带的那个版本。打开你的JDK安装目录,找到bin文件夹,里面有个jvisualvm(Windows下是jvisualvm.exe,Linux/macOS下是jvisualvm脚本),双击就能跑起来。
不过我建议你去VisualVM的官方开源页面下载最新版。JDK自带的版本通常较旧,插件中心和部分新特性支持得不好,新版原生支持更高版本的JDK、界面更流畅,还能安装更多插件。
注意:如果你用的操作系统是macOS,首次启动可能会提示“已损坏”,这是因为没有经过应用商店公证,去“系统设置-隐私与安全性”中允许运行即可。
启动后,左侧的Applications面板会列出一堆正在运行的本地Java进程,默认按PID显示,格式类似25892 com.example.DemoApplication。双击某个进程,右侧会打开一个包含概览、监控、线程、采样器、Profiler等标签页的详情界面。
2.2 界面布局与第一个操作
第一次打开详情页,你会看到“概览”标签,里面展示启动参数、系统属性、JVM参数等信息。在这里可以快速确认进程启动时加了哪些JVM参数,比如-Xmx、-XX:+UseG1GC等。
切到“监控”标签,就能看到CPU、堆内存、元空间、类加载数的实时曲线图。图表右下角有“执行垃圾回收”和“堆Dump”按钮,这两个按钮后面会频繁用到。
我的建议是:刚启动的进程先观察5分钟,让曲线稳定下来,再根据业务特点判断哪些指标是正常的。比如一个服务刚启动时堆内存迅速增长是正常的,因为要加载类、初始化连接池;但如果一路涨到顶部还不回落,就要小心了。
2.3 本地进程“消失”了怎么办
有个场景特别常见:你给某个JVM配置了特殊启动参数,比如-Xmx4g,结果VisualVM的进程列表里就是看不到它。这时候先打开命令行,输入jps -l看看进程是否真的在跑。如果jps能看到、VisualVM看不到,多半是权限问题——比如你是用普通用户启动的工具,而目标进程是用root起的,VisualVM可能无法读取进程信息。
还有一类情况是容器场景。如果Java程序运行在容器里,你从宿主机启动VisualVM,看到的可能只有宿主机上的Java进程,看不到容器内的进程。这种情况下建议直接在容器内装一个VisualVM,或者用后面要讲的远程JMX连接方式,把监控端口暴露出来,从宿主机连进去。
3. 本地监控实战:CPU飙升、堆内存异常、线程阻塞怎么查
工具说白了只是看得见,真正有价值的是怎么从曲线漂移里判断出JVM生了什么病。这一节我按三类高频问题来讲:CPU高、堆内存异常、线程阻塞。全程用的是实际排查过的场景,细节略作调整。
3.1 CPU采样:找到真正吃CPU的方法
CPU飙高是线上最常遇到的问题。如果你面对的是一个正在运行的Java进程,最好的方式不是猜,而是让VisualVM的“采样器”跑一段时间的CPU采样,它会按线程和方法维度告诉你CPU时间花在哪里。
操作路径是:打开进程详情页,切到“采样器”标签,点击“CPU”按钮,等待10到30秒,然后点击“停止”,VisualVM会生成一个采样快照,按线程展示每个方法的采样次数和CPU时间占比。
这里要注意一个常见误解:采样器显示的是“采样命中次数”,不是精确的CPU时间。虽然大部分情况下命中率能反映真实热点,但分配内存比较多、短小轻快的方法容易在采样中被低估。所以看到结果后,不要急着改代码,至少要结合线程栈确认热点方法是否有明显的循环或大对象分配行为。
举一个实际碰过的例子:某个定时任务系统在每分钟整点CPU冲到90%以上,采了几轮样都指向同一个XML解析方法。顺着方法往下看,发现代码在遍历一个超大节点集合时反复调用getElementsByTagName,每次都重新遍历整棵DOM树。把遍历改成一次性缓存之后,整点峰值直接降到20%。这类问题不用压测工具也能找到,VisualVM的采样器够用了。
3.2 堆内存面板:看懂新生代、老年代和元空间
在“监控”标签下,你会看到堆内存曲线。默认展示的是总堆、Used、Eden、Survivor、Old、Metaspace等各区域的占用情况。很多新手只看总堆占用,觉得内存还有剩余就没问题,这是不对的。
GC的工作原理是多数对象先在Eden区分配,Minor GC之后存活对象进入Survivor区,多次存活之后升入Old区。所以你观察曲线时要注意几点:
- Eden区曲线频繁升起又回落:说明Minor GC频繁,通常是短生命周期对象创建太多。
- Old区曲线只涨不跌:说明有对象长期存活且持续增长,大概率是内存泄漏或缓存没有淘汰机制。
- Full GC频繁:Old区满之后触发Full GC,曲线会大幅下降,但很快就又涨回来,这种“锯齿状”曲线很危险,通常离OOM不远。
我接手过一个批处理服务,Old区每10分钟就涨上去触发一次Full GC,每次Full GC要停顿2秒多,业务方反馈任务跑得越来越慢。用VisualVM一看,Old区曲线像心电图一样规律波动。配合堆Dump一查,发现问题是一个全局Map缓存只put不清理,key还是按时间生成的,永不过期。这就是典型的“看起来堆还有空间,实际老年代已经病入膏肓”的情况。
在监控标签页可以直接点击“执行垃圾回收”按钮,手动触发一次GC。这个操作在诊断时很有用——如果手动GC之后Old区能降下来,说明对象还是可回收的,不是泄漏;如果降不下来,那基本可以断定存在泄漏。
3.3 线程面板:死锁、阻塞、锁等待一条条看
切到“线程”标签页,VisualVM会显示所有存活线程的状态,分为Running、Sleeping、Wait、Park等几种。点开任一线程,能看到它的完整调用栈,这几乎是排查线程问题的万能钥匙。
我在实际中最常看的两个点:
第一,Blocked状态的线程多不多。Blocked意味着线程在等锁,大量线程Blocked往往说明锁竞争激烈,可以继续点开看是卡在哪个方法上。如果是synchronized块,方法名会直接显示在栈顶附近;如果是java.util.concurrent.locks,会显示park相关栈帧。
第二,死锁检测。VisualVM提供了自动死锁检测功能,在线程面板中,如果检测到死锁,会在顶部出现“检测到死锁”的提示,并列出形成死锁的线程和它们各自持有的锁。我用这个方法抓到过一个“循环等待锁”的经典案例:两个线程各自持有对方需要的锁,谁也不让谁。直接看线程栈能一眼发现互相等待的关系,修复方式就是统一加锁顺序。
还有一个容易被忽略的:线程数量持续上涨但不下降,这通常意味着线程池没做回收或者有线程泄漏。在VisualVM的概览里可以看到存活线程数,我见过一个系统的线程数从几百慢慢爬到几千,最后直接把内存拖垮。这种问题在VisualVM里表现非常明显——线程数曲线一路向北,然后堆内存也跟着一路向北。
3.4 实际案例:一个定时任务和一次Full GC的排查过程
我自己遇到过最具代表性的一个场景,是一个“白天正常,凌晨4点CPU飙高”的定时任务系统。刚开始以为是定时任务本身逻辑复杂,后来凌晨4点盯着VisualVM看,发现CPU曲线瞬间冲高,采样器定位到热点是一个Excel导出工具的内部方法。
继续看线程栈发现,这个定时任务在导出时每行数据都要走一次反射映射,几千行数据叠加起来,反射开销直接吃满CPU。换成预编译的Bean映射、去掉多余的反射字段之后,任务执行时间从20分钟缩到3分钟,CPU峰值也降到正常水平。
另一个案例是大促期间某服务的Full GC特别频繁,每次Full GC之后线上接口RT就会“抖”一下。用VisualVM远程连上生产环境后,发现堆内存曲线呈锯齿状,老年代涨满就Full GC,Full GC之后就下降。结合线程栈和堆Dump分析,定位到一个大对象在业务代码里被引用到静态变量,业务接口每次调用都会往里面塞数据,而静态变量又没有清理机制。真正展开来看,就是一行代码的事,但不借助VisualVM根本发现不了。
4. 堆Dump与内存泄漏定位:老年代涨满之前抓个现行
如果你已经怀疑存在内存泄漏,单纯看曲线还不够,必须拿到堆Dump去做证据链分析。VisualVM在这个环节非常顺手,但很多细节值得说透。
4.1 如何生成和打开Heap Dump
生成堆Dump有几种方式,各有适用场景。VisualVM本身的按钮最简单:进程详情页的“监控”标签右下角有“堆Dump”按钮,点击就会生成一个.hprof文件并自动打开分析视图。这种方式适合进程还能接受停顿、内存不太大的情况,因为Dump需要暂停JVM并复制堆数据。
如果进程已经OOM或者马上要OOM,再打开VisualVM点按钮可能来不及。正确的做法是提前在JVM启动参数里加上:
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data/logs/heapdump.hprof这样OOM发生时JVM会自动把当时的堆快照写到指定路径,事后用VisualVM打开分析。这是生产环境标配参数,强烈建议所有Java服务都加上,成本极低,收益极大。
如果进程还活着但VisualVM连不上,也可以用命令行工具:
jmap -dump:format=b,file=/tmp/heap.hprof <pid>或者用JDK 8+都支持的jcmd:
jcmd <pid> GC.heap_dump /tmp/heap.hprof4.2 最常用的OQL查询
打开堆Dump后,VisualVM默认进入“概述”视图,能看到每个类的实例数量、Shallow Heap(对象自身占用)、Retained Heap(对象及被引用的子孙对象总占用)。Retained Heap是判断内存大头最直观的指标,一般先从Retained Heap最大的类入手。
真正的高阶操作是OQL查询。VisualVM内置了OQL,类似SQL的查询语法,可比IDEA的Heap Viewer或MAT更直接。举几个常用的查询:
# 查询某个类的所有实例 select * from com.example.MyService # 按实例数量排序 select className, count(*) from heap.objects group by className order by count(*) desc实际排查中我用OQL做过一个特别实破的查询:某个大对象的实例数是业务应该创建数量的10倍,顺着引用链一看,是一个长度为1的线程池在for循环里反复创建,旧对象没有被释放,全部堆积在新生代撑到老年代。这个如果不看OQL,光猜半天也猜不到。
4.3 一个泄漏案例的操作过程
我举一个“看Dump找根因”的完整步骤,你照这个流程走就行:
- 发现Old区只涨不跌,Full GC频率上升到每分钟一次。
- 在VisualVM中点击“堆Dump”,等待生成完成。
- 切到“概述”标签,按Retained Heap排序,找到占比最大的那个类。
- 右键点击类名,选择“查看实例”,然后点其中一个实例,选择“引用”。
- 在引用视图中看谁持有了这个实例,一路追到GC Roots。
我记得有一次追到一个对象,发现是被某个任务队列的任务引用着,仔细看这个任务又引用了整条业务数据链,导致整个“火车车厢”都被扣住无法回收。定位到具体代码后,发现是任务处理完没有清理上下文变量。代码一改,Full GC频率降到一天几次,接口RT恢复稳定。
注意:堆Dump文件通常很大,动辄几百MB甚至GB,打开时VisualVM也会吃不少内存。如果Dump文件特别大,建议在VisualVM中通过菜单“文件-装载”打开,并给VisualVM自身调大
-Xmx。另外,Dump时JVM会暂停一段时间,生产环境操作一定要选业务低峰期。
5. 远程JVM监控:不折腾生产线,用jstatd和JMX连上去
本地能监控已经很方便,但真正的难点在“生产环境怎么连”。VisualVM提供了两种远程连接方式:一种是轻量的jstatd,另一种是功能更完整的JMX。我强烈建议你在测试环境先练熟再上生产。
5.1 jstatd方式:轻量查看,适合测试环境
jstatd是JDK自带的远程监控服务,启动它之后,远端主机会暴露一个RMI服务,VisualVM通过这个服务读取JVM的监控数据。优点是配置简单,缺点是只能看监控数据,不能做线程转储、堆Dump操作。
在目标机器上先创建一个安全策略文件jstatd.policy,内容如下:
grant codebase "file:${java.home}/../lib/tools.jar" { permission java.security.AllPermission; };然后启动jstatd:
jstatd -J-Djava.security.policy=/path/to/jstatd.policy如果只想让特定主机访问,还可以加上-J-Djava.rmi.server.hostname=<本机内网IP>。启动后,在VisualVM中右键“Applications”面板里的“远程”,选择“添加远程主机”,输入目标IP,然后右键这个主机选择“添加jstatd连接”,就能看到远程JVM的监控数据了。
不过jstatd的局限性很明显:很多生产环境出于安全考虑不会跑这种服务,而且它默认使用RMI的随机端口,防火墙配置起来有点烦。所以正式环境我更推荐JMX。
5.2 JMX方式:正式环境更完善的监控通道
JMX(Java Management Extensions)是Java平台标准的监控管理机制,VisualVM的连接方式就是在启动参数中打开JMX端口,然后在本地添加JMX连接。这种方式和Spring Boot Actuator这类监控框架不冲突,反而能相互补充。
目标JVM的启动参数推荐这样配:
-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=9010 -Dcom.sun.management.jmxremote.rmi.port=9010 -Dcom.sun.management.jmxremote.authenticate=true -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.local.only=true -Djava.rmi.server.hostname=<目标机器内网IP>这里有个很关键也最容易踩坑的参数:-Dcom.sun.management.jmxremote.rmi.port=9010。JMX本身打开一个9010端口,但RMI还会随机开一个数据端口用于回传数据。如果不固定这个RMI端口,防火墙就只能开9010,但数据连接走随机端口,必然连不上。把rmi.port和jmxremote.port设成同一个端口,防火墙只需要放行这一个端口即可。
另一个坑是hostname。如果目标机器的/etc/hosts没有把本机IP正确映射到主机名,RMI返回给客户端的地址会是乱走的,导致连接失败或超时。在启动参数里显式指定Djava.rmi.server.hostname可以彻底规避这个问题。
连接方式也很简单:VisualVM中右键“远程”,选择“添加远程主机”,输入IP后,右键主机选择“添加JMX连接”,输入IP:9010,然后填入jmxremote认证所需的用户名密码(如果开启了认证),就能看到完整的监控面板。JMX方式支持采集、堆Dump、线程转储等操作,比jstatd强太多。
5.3 RMI端口不通的排查思路
远程连接最常遇到的错误就是连接超时或拒绝连接。我总结了排查链条,按顺序排查基本能解决:
- 先确认目标进程确实监听了端口,用
netstat -anp | grep 9010看端口是否在LISTEN状态。 - 从本机用
telnet <IP> 9010测端口通不通。不通就去检查防火墙规则和云安全组。 - 通的话还是连不上,重点检查
java.rmi.server.hostname配置。RMI回传的地址不对,VisualVM收到一个非法地址,自然连不上。 - 如果配置了authenticate,确认账号密码文件权限正确,JMX的密码文件需要设置为当前用户只读。权限过大时JVM会拒绝启动或认证失败。
我在地域跨机房连接时踩过一次坑:A机房的VisualVM连B机房的JVM,始终超时,telnet端口又是通的。后来排查发现目标机器有多个网卡,RMI随机端口走了另一个网卡的IP,防火墙只放行了业务网卡端口,才导致数据通道被拒。配上hostname参数后,问题直接消失。
6. 常见问题速查与我的避坑技巧
最后把我在实际使用中遇到的高频问题整理成一张速查表,都是能直接落地的解决方案。
| 问题现象 | 可能原因 | 解决办法 |
|---|---|---|
| 本地进程列表为空 | 权限不足或进程是容器内运行的 | 用jps确认进程,换同一系统用户,或改用JMX远程连接 |
| 远程主机添加成功但监控数据不刷新 | jstatd版本与JDK不一致 | 确保jstatd和业务进程使用同一JDK版本 |
| JMX连接超时 | RMI随机端口未固定 | 加上-Dcom.sun.management.jmxremote.rmi.port并放行该端口 |
| 连接被拒绝 | 认证文件权限过大或hostname配置错误 | 设置密码文件权限600,显式指定hostname |
| 堆Dump文件打不开 | 文件太大导致VisualVM内存溢出 | 调大VisualVM的-Xmx,或改用MAT分析 |
| CPU采样结果不稳定 | 采样时间太短或JVM在频繁GC | 延长采样到30秒以上,避开GC高峰期 |
| 插件中心无法加载 | 网络到更新源不通 | 去官方插件站点手动下载,放到VisualVM的plugin目录后离线安装 |
| 老年代曲线锯齿状 | Full GC频繁 | 及时做堆Dump分析,优先排查Retained Heap大的对象 |
避坑技巧我按优先级列几个:
- 远程连接认证一定要开。生产环境暴露一个不带认证的JMX端口,等于把JVM的操作权限直接送给内网任何一台机器,这是致命的。开authenticate并不难,但很多人图省事就关了,风险极大。
- 观察数据要选业务高峰期。有些指标比如线程数、堆内存占用,在低峰期看着完全正常,到了高峰期才会暴露问题。所以我做定期巡检时,会刻意挑业务流量大的时段去看VisualVM的实时曲线。
- 采样器和Profiler不要同时开。同一个进程上同时跑采样和Profiler会双倍影响性能,还可能让采样数据失真。一次只用一个工具,诊断完立刻停止。
- 不要只看VisualVM一个工具就下结论。把jstat命令、jmap命令、jstack命令的输出和VisualVM的曲线放一起交叉验证,结论才可靠。比如VisualVM显示CPU高,用top -Hp 看线程级CPU也是一层验证。
最后说一个自己总结的用法:遇到JVM相关的怪问题,我会先开VisualVM看整体健康度,确认“CPU、堆、线程、类加载”哪个维度异常,然后针对异常维度做深入采样或Dump分析,再结合业务代码定位根因。整个流程走下来,大部分问题都能在半小时内找到方向。VisualVM不是万能的,但它作为第一梯队的诊断工具,能帮你少走很多弯路,非常值得花点时间练熟。