大家好,我是程序员天天困。
这是「Arthas 线上诊断实战」系列第 3 篇,专门聊 Arthas vmtool。前两篇 watch、trace 都很强,但有一种场景也很常见:字段看着不对,接口却半天没人点,watch/trace 干挂着使不上劲。
vmtool 不靠请求触发,直接从堆里把对象实例捞出来——读字段、调方法、验 Spring Bean。接口半天没人调、定时任务不知道啥时候跑、根本不知道怎么触发的时候,它最好用。点个收藏,我们直接上手。
一、watch / trace 的死角:没请求就盯不住
watch 和 trace 都是「请求触发式」的诊断。 什么意思?

就是得有人调用那个方法,它们才能看到东西。没人调用,监听器就一直空转,啥也看不到。
举个最常见的例子:线上某个本地缓存 Map 越攒越大,你怀疑该清理的 entry 没被清掉,可这个 Map 既没对外暴露接口、也没打日志。watch 盯不住(没人调它的方法),trace 也无从下手。这时候用 vmtool 直接把那个缓存对象从堆里捞出来,看一眼 size() 就知道攒了多少——这就是 vmtool 最值钱的场景。
你可以这么理解:watch / trace 像路口的摄像头,车来了才拍;vmtool 像仓库的钥匙,门不开也能进去翻货架上的货。
二、Arthas vmtool 是什么:给堆里的对象开个口子
Arthas vmtool(JVM 工具命令):Arthas 从 3.5.1 开始带的命令,底层走 JVMTI 接口,能查堆里的对象实例、执行表达式、强制 GC 这些。说白了就是「不用等请求,直接去 JVM 里把对象翻出来看」。
JVMTI(JVM Tool Interface):JDK 提供的原生调试和监控接口,Arthas vmtool 就是靠它去堆里找实例的。
日常排查里用得最多的是 --action getInstances:按类名把堆里还活着的实例找出来,结果放到 instances 数组里,再用 --express 写 OGNL 表达式去读字段、调方法。
可能有人会问:vmtool 和 sc / jad 有啥区别?
sc/jad看的是类本身(类信息、反编译出来的源码);getInstances看的是对象实例。类只有一份定义,实例可以有一堆——你要的是「现在这个 Bean 里字段是多少」,那必须查实例。
IDEA 装了 Arthas 插件的话,在字段上右键,菜单里一般能直接生成 vmtool 命令,省得手敲全限定名。

三、Arthas vmtool getInstances:把私有字段翻出来看
系列还是用 com.ttk 包。假设有个订单统计服务,计数器只有累加和重置,没对外暴露 getter:
@Service
public class OrderStatsService {
private final AtomicLong orderTotal = new AtomicLong(0);
private volatile String lastRegion = "unknown";
public void record(String region) {
orderTotal.incrementAndGet();
lastRegion = region;
}
public void reset() {
orderTotal.set(0);
lastRegion = "unknown";
}
}
线上你怀疑 orderTotal 已经飘了,又不想为了打个日志再发一版。Attach 好进程(第 1 篇讲过),直接来一条:
vmtool -x 3 --action getInstances \
--className com.ttk.service.OrderStatsService \
--express 'instances[0].orderTotal' --limit 5
拆开看:
--action getInstances:按类找实例,不是找类定义。--className:全限定类名。--express:对结果做表达式;instances是官方约定的实例数组。-x 3:展开层级,和 watch 一样,字段嵌套深就往大调。--limit 5:限制返回数量。官方默认是 10,堆里同类实例多的时候记得调小点,别把 JVM 压出问题。
Spring 单例一般只有一个实例,所以 instances[0] 就够了。要是原型 Bean 或者你自己 new 出来的对象,先别加 --express,看下数组有多长,再决定用第几个。
JDK 9+ 的模块系统下,直接打印 AtomicLong 对象时,Arthas 要展开它内部的 value 字段,但受模块导出限制可能报错。更稳的写法是调 .get() 拿到具体值,走的是公开方法,不受影响:
vmtool -x 3 --action getInstances \
--className com.ttk.service.OrderStatsService \
--express 'instances[0].orderTotal.get()' --limit 5

能把没 getter 的私有字段当场读出来,这才是 vmtool 日常最值钱的地方。 平时为了看一个私有字段,要么改代码加 getter 重新发版,要么翻日志猜——vmtool 一条命令直接搞定。
四、对着实例直接调方法:看完了还能动手
既然 instances[0] 已经是那个活对象了,表达式里当然也能调它的方法。比如先手动累一次,再读:
vmtool --action getInstances \
--className com.ttk.service.OrderStatsService \
--express 'instances[0].record("demo-region")' --limit 5
record 返回 void,Arthas 对 void 方法的表达式求值结果通常显示为 null,这是正常的。接着再读字段,确认是不是 +1 了:
vmtool -x 2 --action getInstances \
--className com.ttk.service.OrderStatsService \
--express '{instances[0].orderTotal.get(), instances[0].lastRegion}' --limit 5

学会这招之后,第一反应多半和我一样:"拿到活动的对象了,是不是能在生产上直接动手?" 但冷静想了想--光读字段一般没事,调写方法就等于在线上改状态了。 没二次确认、没回滚预案,手别那么快。
可能有人会问:这和 ognl 命令是不是一回事?
都用 OGNL 表达式,但入口不一样。ognl 一般从静态字段、Spring context 这些已知的地方下手;vmtool 是先去堆里把实例搜出来。想先拿到对象再动手,优先 vmtool。
五、Arthas 查 Spring Bean:从 ApplicationContext 入手
排查里另一个常见的场景:明明加了 @Service,运行时却像没注册一样——NoSuchBeanDefinitionException、注入失败,或者你怀疑拿到的是另一个同名 Bean。
ApplicationContext(Spring 应用上下文):Spring 容器的入口,Bean 的注册和查找都走它。拿到上下文实例后,调一下 getBean(...) 就能看某个 Bean 到底在不在容器里。
先确认堆里有没有上下文实例:
vmtool --action getInstances \
--className org.springframework.context.ApplicationContext \
--express 'instances.length' --limit 5 -x 0
Spring Boot 进程里一般只有一个 ApplicationContext 实例(如果有父子容器,可能有两个)。确认完捞 Bean:
vmtool --action getInstances \
--className org.springframework.context.ApplicationContext \
--express 'instances[0].getBean("orderStatsService")' -x 1 --limit 5
能打出对象,说明容器里确实有;再配合第三节读字段,「Bean 在不在」和「Bean 里现在是什么」就一次说清了。
Fat jar / 多 ClassLoader 的情况下,要是报找不到类,按官方说的补个 --classLoaderClass(比如 org.springframework.boot.loader.LaunchedURLClassLoader),或者先用 sc -d 拿到 classLoaderHash,再用 -c 指定。

六、顺带一提:logger 临时改日志级别
注意:
logger不是 vmtool 的功能,是 Arthas 另一条命令。之所以放在这里讲,是因为排查时它和 vmtool 经常搭着用--vmtool 看清状态后,往往还要把日志打细一点确认分支。
线上日志一般是 INFO,可某个类你怀疑它内部走的分支不对,想看 DEBUG 日志,又不想为了这个发一版:
# 先看当前级别
logger --name com.ttk.service.OrderStatsService
# 直接改成 DEBUG,立刻生效,不用重启
logger --name com.ttk.service.OrderStatsService --level debug
这是类级别的改,只影响你指定的那个类,不会把整个应用的日志都打爆。排查完记得改回去,不然线上 DEBUG 日志量会很大。
七、forceGc 和线上的安全边界
vmtool 还有个 --action forceGc,看名字就懂,强制触发一次 GC:
vmtool --action forceGc
forceGc 真正有用的场景是配合 getInstances 验证回收:先记下某个类的实例数,forceGc 之后再查一次,如果实例数没降,说明这些对象还被 GC Root 持有,是排查内存泄漏的重要线索。排查内存问题时,"强制 GC -> 再查实例数"是个很实用的闭环。
forceGc 不是日常排查按钮。 一般只在怀疑内存泄漏、想配合 vmoption 打开 PrintGC 看回收情况的时候才用。生产高峰期随手敲一下 forceGc,等于给自己制造一次停顿,搞不好业务就抖一下。
官方文档里还有 heapAnalyze、referenceAnalyze、interruptThread、mallocTrim、mallocStats 这些动作,都是内存和线程那块的。本篇先把 getInstances 用熟,内存那些留给后面 profiler / 堆分析那几期。
| 诉求 | 优先命令 | 原因 |
|---|---|---|
| 看某次调用的入参/返回值 | watch | 请求触发,结果直观 |
| 看走了哪条分支、哪层最慢 | trace | 调用树 + 耗时 |
| 没请求也要看对象当前字段 | vmtool getInstances | 直接摸堆内实例 |
| 确认 Spring Bean 是否注册 | vmtool + getBean |
从 ApplicationContext 验证 |
| 临时抬高某类日志级别 | logger | 类级别改 level,不靠发版 |
八、watch / trace / Arthas vmtool 怎么搭配着用
三期写下来,实战口诀其实就一句:
有请求:先 watch 看结果对不对,再 trace 看走哪条路、哪层慢;没请求,或者想看「现在堆里长啥样」:上 vmtool。
| 维度 | watch | trace | vmtool |
|---|---|---|---|
| 触发方式 | 方法被调用 | 方法被调用 | 立刻查堆 |
| 看见什么 | 入参 / 返回 / 异常 | 路径 + 耗时 | 实例字段 / 可调方法 |
| 典型痛点 | 结果不对 | 分支没走到、接口慢 | 没请求、字段飘了、Bean 找不到 |
| 线上风险 | 较低(就是看) | 中等(有开销) | 读字段还好;写方法 / forceGc 风险高 |
推荐的排查顺序是:先看有没有请求能复现 → 有就 watch/trace → 没有或者要看容器里的状态就上 vmtool → 还想看细节日志再临时改 logger。 别一上来就对生产对象调写方法,出了事没法回滚。
结语
Arthas vmtool 补上的,就是 watch / trace 够不着的那一块:不用等请求,直接去 JVM 里把还活着的实例摸出来。会用 getInstances 读字段、会从 ApplicationContext 验 Bean、知道写方法和 forceGc 要悠着点,大半「线上看不清状态」的情况你都能对付了。
下一篇我会接着写这个系列,往 profiler / 火焰图那边走,专门对付「CPU 热点在哪」这种问题。本文参数以 Arthas 官方 vmtool 文档 为准,版本变了以官网为准。
我是程序员天天困,持续分享编程干货。觉得有用的话记得点赞收藏和关注~也欢迎在评论区聊聊:你有没有遇到过这种情况--明明根本没请求进来,却需要把线上某个对象的状态看清楚?你是怎么排查的?