
Arthas 里 sc 和 sm 这两条命令我用过很多次。如果你整天跟线上 JVM 故障打交道一定遇到过这种情况代码里明明引用了某个类可运行起来行为完全不对或者从日志堆栈里看到某个方法抛异常但你不确定当前 JVM 里加载的到底是哪个版本的类。这时候 scSearch Class和 smSearch Method就是排查问题最趁手的工具。很多新手拿到 Arthas 第一反应就是 watch、trace、dashboard这套组合拳确实亮眼。但实操多了你会发现真正难的不是增强逻辑而是搞清楚“当前 JVM 里到底有什么”。sc 和 sm 就是这层地基——它们一个查类、一个查方法不会修改任何行为、不会插入增强逻辑纯粹做信息探测。先把这两个命令吃透后面用 watch、tt、trace 才能精准命中目标不然经常是方法名写错、类名不匹配增强了个寂寞。这篇文章我会把 sc 和 sm 的完整用法拆开讲从最基本的查询到高阶的正则匹配和类加载器排查配合几个我实际踩过的故障案例把每个参数为什么要加、每行输出代表什么含义都讲明白。适合刚接触 Arthas 想快速上手的人也适合已经用过 watch 但遇到“找不到目标类/方法”困扰的同学。1. 两条命令到底解决什么问题1.1 先搞清楚它们的定位sc 是 Search Class 的缩写作用是在 JVM 已加载的类集合里搜索目标类。注意关键词是“已加载”——它搜的不是磁盘上的 class 文件也不是 jar 包清单而是当前 Java 进程内存里真实存在的那份类元数据。sm 是 Search Method 的缩写作用是在指定类里查找方法。默认会列出类中所有已声明的方法包括方法签名、修饰符、参数类型这些关键信息。很多人以为看方法就是用 javap 反编译但线上环境你根本没有 class 文件路径而且 JVM 里可能同时存在多个同名类javap 只能看到你本地的那个版本sm 看到的是进程里实际运行的那个版本这才是符合真实战场的信息。举个例子你本地代码里有三个重载的 query 方法线上运行的根本不是你本地这份代码你用 javap 看到的签名毫无意义。但通过 sm 查出来的是当前 JVM 里真实注册的方法签名配合 watch 下断点时方法名和参数类型必须严格按照 sm 的输出写否则增强命令直接报错。1.2 什么场景下优先用它们我在实际排查中遇到这几种情况一定会先用 sc / sm 开路第一种是多版本 jar 冲突。典型表现是启动正常但运行到某个方法时抛 NoSuchMethodError 或者 NoClassDefFoundError。代码编译期用的是新版本的类方法但运行时 ClassLoader 加载的是旧 jar 里的类方法老版本里根本没有。这时候 sc -d 看类加载路径就能一眼锁定加载来源。第二种是确认发布是否生效。线上发布后总有人怀疑“代码是不是没更新上去”与其问别人不如自己查。通过 sc -d 看目标类的 ClassLoader 和代码来源 URL如果这个类是从旧的 jar 路径加载的那就是发布包有问题。第三种是写 watch / tt / trace 之前做侦察。Arthas 这些动态增强命令都需要精确匹配类名和方法名。网上很多教程直接甩出一个 watch 命令让你复制但你项目里的包名、方法重载、接口实现类都不一样直接抄作业必然失败。正确姿势是先用 sc 找到类的全限定名再用 sm 看方法完整签名然后把签名填进 watch。第四种是排查类加载器问题。比如应用程序里同一个全限定名的类被多个 ClassLoader 各自加载了一份行为互相隔离用 sc -d 可以看到每个实例的 ClassLoader 哈希值这是后续加载器排查的重要线索。1.3 这两个命令不会动线上业务这是我最想强调的一点。sc 和 sm 本质上是查询类它们不会修改类的字节码不会插入切面逻辑运行期间不会对业务产生任何副作用。但 watch、trace、tt 不同这些命令会在目标方法上做字节码增强虽然轻量但频繁使用还是会对性能有影响。所以排查问题的推荐顺序是先 sc 确认目标类加载情况 → 再 sm 确认目标方法签名 → 最后按需用 trace 或 watch 做精细观测。前两步是零风险的信息摸底哪怕在高并发核心链路中执行也没有任何压力。2. 核心参数实战拆解2.1 sc 的基础用法与 -d 参数最基本的用法就是 sc 加类名。不过类名有个细节多个同名类存在时结果会列出全部。默认不带 -d 的时候输出只显示类名和是不是接口信息量很小实际使用时我极少用裸 sc。真正有用的是 -d 参数它会展示这个类的完整详情包括类加载器、类的注解、继承的父类、实现的接口、甚至类文件的具体来源 URL。我贴一段实际输出类名已按脱敏要求处理$ sc -d com.example.service.OrderService class-info com.example.service.OrderService code-source /opt/app/lib/order-service-1.0.0.jar name com.example.service.OrderService isInterface false isAnnotation false isEnum false isAnonymousClass false isArray false isLocalClass false isMemberClass false isPrimitive false isSynthetic false simple-name OrderService modifier public annotation interfaces super-class java.lang.Object class-loader sun.misc.Launcher$AppClassLoader class-loader-hash 1b6d3586 classLoaderHash 1b6d3586这里的 code-source 极其关键。它告诉你当前 JVM 加载的这个类是从哪个 jar 里来的这一行信息在排查多版本冲突、发布未生效问题时就是决定性证据。class-loader 和 classLoaderHash 则展示了类的加载来源遇到多 ClassLoader 场景时用哈希值做区分。建议把 sc -d 理解为“类的体检报告”凡是需要了解某个类在 JVM 中的真实状态就直接用这个命令。2.2 sm 的参数与注意事项sm 的默认输出就是方法签名列表按照方法声明的顺序展示。例如$ sm com.example.service.OrderService com.example.service.OrderService init() com.example.service.OrderService processOrder(java.lang.String, int) com.example.service.OrderService queryDetail(java.lang.String) com.example.service.OrderService queryDetail(java.lang.String, boolean) com.example.service.OrderService cancelOrder(java.lang.String)Legacy注意两点第一这里显示的方法名后面跟着完整参数类型这个签名正是 watch、trace 命令需要使用的格式。第二方法名带 Legacy 这种后缀的是编译器生成的桥方法排查时看到它不要慌它只是泛型擦除后系统自动产生的不是你的业务方法。sm 同样支持 -d 参数会显示更细节的方法信息包括方法的修饰符、声明注解、参数注解、异常类型等。这个在需要确认某个方法是否带 synchronized、是否被特定注解标记时很有用。另外 sm 还支持第二个参数直接指定方法名例如 sm com.example.service.OrderService queryDetail更精准。但如果方法有多个重载不带 -d 时会把所有同名的重载都列出来正好方便你观察重载关系。2.3 正则匹配的高阶玩法sc 和 sm 都支持 -E 参数用正则表达式匹配类名或方法名。这在实际排查中太实用了因为线上类全限定名往往很长完整输入容易拼错用正则做模糊匹配可以快速圈定范围。比如我有一次需要找到所有处理订单相关的类直接执行$ sc -E .*order.*Service com.example.service.OrderService com.example.service.OrderQueryService com.example.service.OrderWriteService这三行结果直接帮我锁定了候选类比一个个敲全限定名高效太多。注意 -E 是大小写敏感的如果类名首字母大写而你写了小写结果就会为空。正则是要匹配完整全限定名而不是简单包含关系所以开头结尾要加好点号加星号。当字段特别多时sc 还可以配合 -f 参数输出类的字段信息这在你需要确认一个类到底有哪些属性时比较有用。它会把所有字段名和类型展示出来省得再去翻源码或者反编译。3. 完整实战案例一次“方法找不到”的线上事故3.1 事故现场与最初的排查方向某个周五下午一个订单服务的同事在群里反馈他们刚发布完一个版本结果服务启动后第一次请求就直接报错堆栈关键信息是 NoSuchMethodError指向 OrderService 的一个 queryDetail 方法。同事第一反应是“代码没问题啊编译都过了”但错误明明就是运行期找不到方法。重新编译、清理本地缓存、重启服务试了一圈没用。最后我直接登上测试机准备用 Arthas 看一眼 JVM 里真实加载的类是什么情况。这种问题靠看日志是看不出真相的必须到运行环境中确认。3.2 用 sc -d 锁定加载来源首先用 jps 找到目标进程 pid然后 attach 进 Arthas。第一步当然就是看类加载情况$ sc -d com.example.service.OrderService输出里 code-source 这一行直接说明问题——OrderService 是从 order-service-0.9.8.jar 加载的而本地编译用的明明是 1.0.0 版本。也就是说代码虽然编译通过但运行环境里实际生效的还是旧 jar。这不是在源码里能看出来的问题因为源码看起来完全正确问题出在发布包组装时用了依赖锁定的旧版本或者多个模块之间依赖传递导致最终打进去的 jar 不是你以为的那个版本。3.3 用 sm 确认方法存在性确定类加载的是旧版本后再用 sm 查看目标方法是否存在于当前 JVM 中$ sm com.example.service.OrderService queryDetail输出为空。这个方法的签名在新版本里是 queryDetail(String, boolean)但旧版本里根本没有这个方法所以 JVM 运行时沿着父类去找也找不到直接就 NoSuchMethodError 了。这里有个排查技巧如果在旧版本类里确认新方法不存在还可以看一下这个类有没有被其他 jar 里的同名类覆盖。有一次我遇到的情况是自己项目里的类是对的但依赖树里另一个模块也发布了同名类ClassLoader 按照 jar 的加载顺序把别人家的类加载进来了。用 sc -d 会发现 code-source 指向了一个完全不相干的 jar。3.4 根因确认与修复后来检查发布脚本发现打包时一个依赖模块没有重新构建导致发布了包含旧版本 OrderService 的 jar。重新构建依赖模块、替换发布包后再次用 sc -d 验证 code-source 已经指向 order-service-1.0.0.jar再用 sm 查询方法就正常返回了签名信息。这个案例充分说明了 sc 和 sm 在验证“类是否加载正确”这个问题上的价值。遇到 VerifyError、NoSuchMethodError、NoClassDefFoundError 这类运行时类加载异常第一反应不应该是去翻日志而是直接 sc 看加载源头基本一次就能锁定问题根源。4. 复杂场景下的深度玩法4.1 同一个类被多个 ClassLoader 加载多 ClassLoader 场景在普通业务项目里不常见但在框架容器、中间件、热部署环境里经常出现。同一个全限定名类可能被 AppClassLoader 加载一份又被 Tomcat 的 WebAppClassLoader 加载一份两份之间完全隔离。比如分布式调度框架里任务代码和框架代码可能处于不同的类加载器层级。你用 sc 查一个类返回了多行结果每行的 classLoaderHash 都不同。这就是多个加载器重复加载了同名类的最好证据。遇到这种结果处理方式就要看目标如果你是想要修改行为必须确认你 attach 的 agent 和业务类处于同一个加载器上下文如果是想确认某个加载器中的版本就要记录 classLoaderHash 并用 classloader -c hash 命令进一步查看这个加载器的详情和加载路径。4.2 Spring 代理类导致的“找不到方法”陷阱Spring 项目里有一个非常经典的坑业务类被 AOP 代理后你 sc 查到的可能是代理类而不是你的原始类。比如$ sc -E .*OrderService.* com.example.service.OrderService com.example.service.OrderService$$EnhancerBySpringCGLIB$$abc123第二行就是 CGLIB 生成的代理类。如果你用 sm 直接查代理类的方法会发现你原来业务方法被转成了 CGLIB 的拦截逻辑方法签名也可能变了比如参数变成了 MethodInterceptor 相关签名签名对不上自然一脸懵。这种情况下的正确做法是对原始类做增强操作。Arthas 在处理方法增强时会自动剥掉这层代理吗不会。所以你要么直接对代理类下命令要么在 watch 的时候把类名替换成原始类名。关键技巧是先手动确认当前 Spring 容器里的 bean 到底是那个类再用 sc -d 看它的 super-class顺着继承链找到你真正的业务方法所在类。我刚入行时就在这里浪费过半天时间明明代码里有 showName 方法sm 却死活查不到后来才发现是 CGLIB 代理类压根没直接暴露这个方法。所以记住sc 和 sm 看到的是 JVM 的真实结构不是你的源码结构。代理、增强、桥方法都会影响结果理解这一点比背诵命令参数更重要。4.3 重载方法如何区分Java 方法重载是常见现象线上排查方法问题时往往会面对三个同名的 queryDetail它们参数类型不同。如果你要用 watch 观测其中一个就必须指定完全匹配的签名。sm 的搜索结果会把这些方法全部列出来但默认不带 -d 只显示签名。要看更完整信息比如每个方法的修饰符和注解就需要加 -d$ sm -d com.example.service.OrderService queryDetail控制台会返回每个重载的详细说明。有一个细节值得注意带可变参数的方法String...在 JVM 里的类型显示为 java.lang.String[]跟数组参数显示一样。写 watch 时如果用的形式参数类型和 JVM 不一致就提示找不到方法需要自动转成数组形式。4.4 泛型与桥方法的干扰泛型擦除是另一个干扰源。代码里定义的是 List 字节码里真实类型是 List。编译器可能生成桥方法保证多态所以 sm 查出来的方法会比源码里多。排查时先不要被这些后缀为 Legacy 或者参数类型看起来怪怪的方法干扰优先找匹配你业务逻辑的那个。桥方法还有一个特点修饰符通常包含 synthetic。如果 sm -d 输出中看到 synthetic 或 bridge 标记就要意识到这是编译器生成的辅助方法不是你业务代码里的方法。把它忽略掉继续找真正的方法签名即可。5. 常见问题与实操经验速查5.1 高频踩坑点对照排查问题最怕的就是命令跑完没结果又不知道哪里写错。我把常见失败情况整理成了一张对照表问题现象最可能的原因验证方式sc 结果为空类名拼写不全、未带包名用sc -E .*类名.*模糊匹配类确实存在但 sm 结果为空类还没被 JVM 真正使用触发类加载先访问一次业务接口再查sm 显示的方法和源码不一致Spring/CGLIB 代理类或编译器桥方法干扰sc -d 看 super-class 后重新定位多个同名类展示多版本 jar 在企业依赖中并存对比每条结果的 code-source写 watch 提示找不到方法方法参数类型未按 JVM 内部签名写以 sm 输出为准可变参数写成数组最坑的是第二种情况JVM 的类加载是懒加载机制。一个类如果还没有被代码真正使用到它就不会出现在 sc 的搜索结果里。这跟“类不存在”是两回事。判断标准很简单先调用一下相关的业务入口然后再重新 sc看是否有结果。5.2 一个值得长期使用的操作习惯我习惯在排查每个方法相关问题时固定走一套流程先 sc 类 → 再 sc -d 确认 classLoaderHash 和 code-source → 然后 sm 类和方法名 → 确认签名 → 再开始 watch / trace。这几步看起来繁琐但能过滤掉八成以上的错误操作。尤其推荐将 sc -d 作为验证类加载来源的默认动作。很多人排查每日构建问题时耗时很久其实一条 sc -d 就能告诉你这个类到底加载自哪个 jar、哪个目录。如果是 Spring Boot 场景看 code-source 的路径还能区分是 fat jar 里的解压临时目录还是外部 class 目录。5.3 和其它命令配合的实战方案sc 和 sm 最常配合的“黄金搭档”是 watch 和 trace。典型流程是$ sc -d com.example.service.OrderService $ sm com.example.service.OrderService queryDetail从第二步的输出结果里拿到完整方法签名再把它填到 watch 的参数里。如果 queryDetail 有重载watch 可以限制只抓取某一个重载方法这样数据噪声会小很多。trace 命令的使用也类似需要指定类名加方法名。用 sm 验证过的方法trace 执行几乎不会失败。反之如果方法名和参数类型有任何偏差trace 会直接报错拒绝执行。把 sc / sm 当“前置侦察兵”后面所有增强命令的成功率都会大幅提升。5.4 最后分享一个现场救急技巧有一次线上排查日志里反复报某个类的方法找不到但我 sc 以后发现目标类压根没有加载问题就变成了“谁应该加载它但没有加载”。这种排查思路也不复杂用 sc -E 去搜相关模块的其他类确认那个模块整体的加载情况如果其他类也没加载说明整个模块被启动逻辑跳过了。再配合 jstack 看一下当前线程栈通常很快能定位到初始化逻辑的时序问题。如果你用 Arthas 有一段时间了还可以试试 sc -d 到 classLoaderHash 后直接用 classloader 命令查这个加载器都加载了哪些 jar对判断类加载顺序非常直观。这一招在多模块项目里尤其管用因为有时候你以为自己发布的是 A 模块的代码实际生效的却是 B 模块中携带的旧版 A 类。我在实际使用中体会到很多看似复杂的方法运行异常根因都是类加载层面的“货不对板”。sc 和 sm 能让你直接看到 JVM 内部那份货到底是什么这种洞察力是翻再多日志都换不来的。把这两条命令用熟了你排查线上问题的效率会有一个非常明显的提升。