排查线上 Java 问题的时候很多人习惯一上来就是 watch、trace、stack 三板斧把精力全部押在方法调用链上。但我在实际调试里反而更频繁地先敲两个看起来没什么技术含量的命令sc 和 sm。它们解决的问题很朴素——类到底加载了没有、被谁加载的、方法签名到底是什么。可恰恰是这两件事常常决定你后面是继续追查还是白忙一场。这篇文章把我平时怎么用 sc、sm 的完整思路拆一遍包括参数含义、输出字段、实际案例和常见坑适合正在排查类加载冲突、方法签名不确定或者准备做热更前确认信息的 Java 开发者。1. 我在排查链路里靠这两条命令回答什么问题先不急着讲参数说一下为什么我会在关键时刻依赖它们。很多人把 sc、sm 当成“查 API”用的工具觉得看看类名、方法名而已没什么含金量。但真实线上问题里这俩命令的价值恰恰体现在一些容易被忽略的底层细节上。1.1 类是否存在、是否被加载这个“基础问题”有多容易被忽略Java 是懒加载的JVM 启动时并不会把所有类一口气载入内存而是到了某个类第一次被引用时才触发加载。于是很多线上问题会表现得很诡异你本地编译时类都在但运行时却抛 ClassNotFoundException 或 NoSuchMethodError。这种时候你用 watch 去追踪方法是没用的因为类可能压根没有加载方法调用链还没建立。sc 的作用就是把 JVM 里已加载的类按照全限定名搜出来让你先确认“它到底存不存在、在不在当前进程里”。我遇到过一个情况一个奇怪的报错只在凌晨定时任务里出现白天手动复现完全不报错。后来才发现白天某个业务请求已经触发过一次类加载而凌晨任务是新起的执行路径走的是一批尚未懒加载的类ClassNotFoundException 只在那个路径里炸出来。先用 sc 搜索确认类确实没加载排查方向一下子就清晰了。1.2 类加载器和同名类冲突的初步定位同名类冲突是另一个高频场景。同一个全限定名可能同时来自两份不同的 jar 包或者被不同的类加载器各加载了一份。这类问题最恶心的点在于你看着代码里只有一个类但运行环境里其实有多个副本方法签名稍微对不上就报 NoSuchMethodError。sc 的输出里有一列class-loader和class-loader-hash能把每个加载实例的类加载器类型和哈希值暴露出来。如果你用 sc 搜索一个类名出现了多行输出说明 JVM 里不止一份字节码后面就可以顺着 class-loader-hash 逐个确认到底是谁加载的。这个信息在普通 IDE 里是看不到的但往往就是问题根源。1.3 方法签名确认重载方法和 AOP 增强的“照妖镜”sm 命令回答的是另一个维度指定类里到底有哪些方法方法签名长什么样。它在两种场景下特别有用。第一种是重载方法太多你想确认目标参数类型是什么避免 watch 的时候写错签名导致命令半天没反应。第二种是怀疑方法被字节码增强或者代理包装了用 sm -d 能看到修饰符、返回值、参数、异常声明如果发现某个方法被改成了 final 或者多了额外的参数那基本可以判断有框架干预。我把常用场景和命今选择整理成了一个表方便你对照排查场景首选命令关键输出类是否存在、是否被加载scclass-info 行是否出现同名类冲突sc多行 class-loader-hash确认类加载器类型sc -dclass-loader 具体实例查看接口、父类、注解sc -dinterfaces / super-class / annotations查看成员变量与静态变量sc -d -f -xfields 列表与遍历深度确认某个类的全部方法sm 类名方法签名列表查看重载方法具体参数sm -d 类名parameters、modifier 等找构造方法sm 类名 或 sm -d 类名init方法这个表是我日常排查的基本索引下面的内容就按这个逻辑逐个展开。2. sc 命令拆解匹配规则、输出字段和 -d/-f 的真实含义sc 是 Search Class 的缩写作用是在目标 JVM 里搜索所有已加载的类。它支持模糊匹配、正则匹配也能用参数输出类的详细信息。很多人把它当成一个简单的“看看类在不在”的命令其实它的参数组合能挖出不少底层信息。2.1 匹配模式通配符、精确匹配和正则的边界sc 默认使用通配符匹配支持*匹配任意多个字符?匹配单个字符[]匹配字符集合。比如sc com.demo.*是匹配 com.demo 包下所有已加载的类sc *Controller匹配所有以 Controller 结尾的类。一个需要注意的细节是如果你输入的是一个精确的全限定名比如sc com.demo.OrderService那匹配的就是这个类本身但如果只是简单类名OrderService它会被当作包含匹配来处理可能匹配到不同包下的多个同名类。所以搜不到或者搜到多余结果时先检查自己有没有写全限定名这是最common的输入问题。如果默认通配符满足不了需求可以加-E开启正则匹配。比如sc -E com\.demo\.service\..ServiceImpl就可以精确控制命名模式。正则模式下就不再支持*和?这种通配符语义了两者不要混用否则你会得到一堆莫名其妙的空结果。2.2 普通输出里除了类名还藏着类加载器线索不带参数的 sc 输出很简洁每组信息包含三行class-info类的全限定名这是核心。class-loader类加载器的类型和实例引用能看出是启动类加载器、扩展类加载器还是应用类加载器或者某个框架自定义的加载器。class-loader-hash类加载器实例的哈希值Arthas 里很多命令都需要用这个值来定位同一个加载器。如果同一时间出现多组这种信息说明同一个类被多个加载器各加载了一次。这就是冲突的雏形。比如你搜一个 SDK 里的核心类结果出来两条记录一条 class-loader 是应用类加载器一条是某个容器框架的加载器那基本可以断定你的应用依赖里存在两份不同版本的 jar。2.3 -d 详细输出interfaces、super-class、annotations 才是排查重点加上-d参数后sc 会输出一个类的完整骨架信息包括interfaces当前类实现的所有接口名。super-class直接父类全限定名。annotations类上的注解列表。fields成员变量信息不指定-f时通常只显示字段名和类型。class-loader-hash与普通输出一样用来定位加载器。排查 AOP 不生效时这个输出特别好用。你可以一眼看出类有没有被框架生成代理类接口列表里是否多出来 Spring 风格的Proxy相关接口或者父类是否变成了某个 CGLIB 增强类。我自己有一次排查事务不生效的问题就是用 sc -d 看到目标类的 super-class 变成了一个奇怪的增强类才意识到是某个组件在启动时替换了原始 Bean 定义。2.4 -f 与 -x字段信息和静态变量遍历深度sc 还有两个经常配套使用的参数-f和-x。-f表示输出当前类的成员变量信息但它一般需要和-d配合使用单独加-f输出信息是非常有限的。-x是用来指定静态变量的属性遍历深度默认是 0只能输出一层如果字段里有复杂对象可能需要把深度调大才能看到嵌套结构。比如想看某个配置类的所有字段可以用sc -d -f -x 2 com.demo.RuntimeConfig。这样不仅能列出字段名和类型字段对象内部的属性也会递归展示两层。不过这个命令的输出会非常长如果类里有缓存集合之类的字段控制台滚动会让你怀疑人生。所以实操时我一般先不加 -x看到字段名之后再用 ognl 命令针对单个字段取值这样更可控。3. sm 命令拆解方法签名、构造方法和 -d 的完整解读sm 是 Search Method 的缩写用来搜索某个已加载类声明的方法。它和 sc 是一对孪生命令sc 管“类在不在”sm 管“方法在不在、长什么样”。很多和 NoSuchMethodError 相关的问题最后都是靠 sm 拿到准确签名才确认下来的。3.1 基本用法一个类名搜出的方法签名列表sm 最简单的用法是sm com.demo.OrderService不带任何参数时它会列出这个类声明的所有方法签名。每一行包含方法名、参数列表和返回值类型格式像是简化版的 Java 方法声明。例如java.lang.String getOrderById(int orderId) void updateOrder(com.demo.Order order)这个输出适合快速浏览类里有哪些方法。如果你只关心某一个特定方法可以在类名后面再加一个方法名作为过滤条件比如sm com.demo.OrderService getOrderById只显示目标方法省得在长列表里翻。注意一点方法名匹配也走通配符逻辑sm com.demo.OrderService get*可以匹配所有以 get 开头的方法。这个方法名过滤是大小写敏感的别把 get 写成 Get否则直接空结果。3.2 -d 详细输出修饰符、参数、异常和注解sm -d会把每个方法的信息展开成结构化字段常见的输出包括declaring-class定义这个方法的类注意可能是父类而不是当前类。method-name方法名。modifier访问修饰符和特殊标志比如 public、static、final、synchronized。parameters参数类型列表这个最容易踩坑因为泛型信息往往会被擦除实际运行时的参数类型和源码里写的不完全一样。return返回类型。exceptions方法声明抛出的异常如果没声明就是空。annotations方法上标注的注解排查切面是否匹配方法时很关键。我特别喜欢看declaring-class这行。如果一个类没有自己声明某个方法但你在调用时又不报错那很可能方法是从父类或者接口默认方法继承下来的。sm -d 会把声明来源直接指出来省得你顺着继承树一层层翻源码。3.3 构造方法为什么不叫类名而叫init用 sm 看构造方法时很多新手会愣住以为命令出 bug 了。其实 Arthas 把构造方法统一显示为init。想查构造方法直接sm com.demo.OrderService列表末尾会看到init形式的条目想看详细信息就用sm -d com.demo.OrderService init。构造函数的参数列表在 -d 输出里同样清晰可见。这个在排查框架实例化对象失败、或者依赖注入构造函数参数类型不匹配的问题时非常有用。有一次一个项目在启动时报创建 Bean 失败我怀疑是构造函数参数类型从 Integer 变成了 int用 sm -d 一看参数列表里的类型和预期完全不一样根源一下子就找到了。3.4 重载方法太多时怎么精准定位目标方法重载是 Java 的特性但也成了排查时的烦恼。一个类里有三四个同名方法分别接收不同参数你要是不确定实际调用的是哪个watch 写错签名就白等了半天。我会先执行sm -d com.demo.PaymentService pay把所有同名过载方法的结构化信息全部列出来再对比参数类型和注解挑出最可疑的那一个。拿到准确签名之后再把这个签名原封不动地填到 watch 表达式里。这一步看起来多花了几秒钟实际能省下大量反复猜测的时间。4. 实战记录一个 NoSuchMethodError 的完整排查链路理论讲再多不如走一遍完整案例。这个案例来自一个模拟项目叫它模拟项目 X现象是某个定时任务在运行到一半时突然报 NoSuchMethodError但同一个项目里的另一个服务却能正常执行。下面就是我当时的排查全过程。4.1 现象与初步判断报错栈指向一个第三方工具类的某个方法提示找不到某个方法签名。一开始我以为是版本升级不兼容直接在依赖里查一遍发现 p om 里引用的确实是新版本理论上方法应该存在。于是问题变成了编译期依赖和运行期加载到的方法不一致最典型的嫌疑就是依赖冲突或重复加载。4.2 sc 搜索发现同名类被多个加载器加载先启动 Arthas 挂上目标进程然后执行sc com.example.Toolbox。结果出乎我意料屏幕上一次性出现了两组输出每组都包含class-info、class-loader和class-loader-hash而且两组信息对应不同的类加载器哈希。这意味着 JVM 里确实存在两份 Toolbox 类。其中一组的 class-loader 是标准的应用类加载器另一组则来自框架的组件加载路径。这个项目用了模块化部署不同模块之间隔离得比较厉害所以同一个类可以被加载两次。4.3 用 class-loader-hash 和 -d 参数确认两份字节码的差异接下来我通过sc -d com.example.Toolbox查看两份信息的详细字段。由于输出中有两个块的class-loader-hash不同我分别用-c参数指定加载器哈希来单独看某一侧的详情比如sc -d -c 1be9118c com.example.Toolbox。左侧的块显示这个类实现了一个新的接口右侧的块显示这个类还是旧接口。也就是说两份字节码不仅加载器不同连实现的接口都不一样。那问题就清楚了一半运行中调用的对象实例很可能来自旧版本的那一份自然找不到新版本才有的方法。4.4 用 sm 对比方法签名差异为了让结论更确凿我分别指定两个类加载器哈希去执行 sm依次查看方法列表。命令格式是sm -d -c hash com.example.Toolbox。两份输出对比下来旧版本缺方法的现象一目了然新版本里那些新增方法的签名在旧版本中一个都不存在。到这里根因已经确定同一个类存在新旧两个版本定时任务模块的类路径解析到了旧版本。NoSuchMethodError 不是代码 bug而是运行时加载错了类。4.5 修复方式与验证修复方案是调整依赖把传递引入旧版本的依赖排除掉只保留一份统一版本。重新发布之后再挂 Arthas 执行sc com.example.Toolbox这次只剩一组输出sm 列表里的方法签名也和预期一致。定时任务恢复正常。这个案例里的每一步都脱离不开 sc 和 sm。先搜类确认存在和加载器再用 -d 和 -c 拿到细节最后用 sm 对比方法签名整个链路走下来非常顺畅。比直接在 IDE 里翻半天依赖树要快得多。5. sc/sm 的高级组合玩法管道、watch、jad、ognl 一条龙单独用 sc/sm 能解决的问题有限它们真正厉害的地方在于和 Arthas 其他命令配合起来形成一条完整的排查链路。下面这几个组合是我日常使用频率最高的。5.1 在结果集里用管道快速捞重点Arthas 控制台支持直接使用管道符号最常用的是| grep、| wc -l、| head。比如你想统计某个包下到底加载了多少个类可以执行sc com.example.service.* | grep class-info | wc -l这个输出结果非常直接能快速判断某个包的类加载情况。如果你嫌 sc 输出结果太多也可以在命令尾部加| head -n 20只看前 20 条。这里有个体验小坑Arthas 的每一条命令执行完还会输出 Affect(row-cnt:N) 这样的统计行用 grep 过滤时容易被它混进来。所以过滤条件一定要够具体比如只 grepclass-info宁可多过滤一层也不要贪图省事。5.2 拿到方法签名之后直接喂给 watchsm 输出方法签名之后最常见的一个衔接动作就是把这个签名原封不动地填到 watch 表达式里。例如先执行sm -d com.example.OrderService submitOrder看到参数列表是int, java.lang.String后再执行watch com.example.OrderService submitOrder {params[0], params[1]} -v -n 5这样写在表达式里的参数索引就和实际方法签名一一对应不会出现params[2]越界或者类型不匹配的尴尬。这个习惯看着简单但在高并发接口排查时能避免很多误导性的空输出。5.3 反编译前先确认加载的是哪份字节码用 jad 反编译某个类之前强烈建议先用 sc -d 看一眼 class-loader-hash。因为 jad 反编译的是当前 JVM 里实际加载的那份字节码不是你在 IDE 里看到的源码。如果类存在多份副本jad 结果可能和你预期不一致。正确的姿势是先sc -d com.example.Demo拿到目标类的 class-loader-hash然后jad -c hash com.example.Demo。这样反编译出来的代码才准确对应运行时行为你根据这份代码去做热更或者排查才不会跑偏。5.4 配合 ognl 读取静态变量和字段值sc -d -f -x 能告诉你一个类有哪些字段但要看字段的具体值还需要 ognl 出场。比如先看到配置类里有retryTimes这个字段就可以用ognl -c hash com.example.RetryConfigretryTimes把运行时的真实值捞出来。这个组合很适合排查配置没有生效、或者缓存字段被某个线程改掉的问题。需要注意的是ognl 命令也要指定 class-loader-hash最好从 sc -d 的输出里复制别凭感觉乱填。6. 使用 sc/sm 时容易踩的坑工具好用但坑也不少。这些坑不是命令本身的问题更多是使用者对 JVM 加载机制、匹配规则的理解不到位导致的。我把最常见的几个列出来。6.1 类没加载时的“假阴性”sc 搜不到类不代表类真的不存在。因为 Java 懒加载可能这个类从启动到现在就没被引用过所以还没进 JVM。遇到这种情况先别急着下结论说类缺失。你可以先搜一下同包的其它类如果其它类能被搜索到说明 Arthas 侧没有问题问题只在于目标类确实没加载。想触发类加载可以先用 jad 或者 watch 去碰一下或者直接在业务里临时加一段调用代码再观察。另外如果你搜的是接口名也要记住接口本身可能只在被实现时才会加载有的接口甚至只在反射场景下才会被运行时创建。6.2 输入大小写、空格和转义的细节Arthas 的类名匹配是大小写敏感的。全限定名里的包名分隔符是英文点号不能写成中文句号或者斜杠。如果你用正则模式还需要注意点号需要转义成\.否则它会被当成“匹配任意一个字符”结果不可控。另一个容易踩的细节是在 Arthas 控制台输入*通配符时如果类名里有$这种特殊字符比如内部类Outer$Inner建议直接用单引号包住整个类名避免 shell 层面的特殊解析。虽然不是每个环境都会出问题但养成加引号的习惯能避免不少莫名奇妙的失败。6.3 接口、抽象类和 Lambda 生成类的搜索差异搜接口和抽象类时sc 能搜到它们本身但不会自动展开所有实现类。你想找实现类得自己猜包名或者用*Impl之类的通配符去匹配。这一点跟 IDE 里“查找实现类”的体验差别很大容易产生“怎么找不到”的误解。Lambda 表达式生成的类也值得一提。它们的类名是xxx$$Lambda$123/0x...这种形式直接搜业务方法名肯定搜不到。要排查 Lambda 相关的问题用sc *Lambda*先看有哪些 lambda 类然后用 jad 反编译那些类来确认实现内容。6.4 输出太多时的限流策略在类特别多的应用里直接敲sc不带参数会把整个 JVM 的已加载类全部打出来控制台直接翻屏本机内存和 CPU 也会被拖累。正确的做法是缩小范围先按包名精确匹配不要一上来就*通配。配合-d参数时输出会更大建议加上-n限制匹配数量比如sc -d -n 5 com.example.*只显示前 5 个类避免一条命令把 Arthas 服务端搞到卡顿。排查时先看少量样本再逐步聚焦到具体类远比一把梭更高效。我最后想说的是用 sc/sm 排查问题最大的收益其实不是“找到某个类”而是让你在动手 watch、trace 之前就把运行时的真实状态搞清楚。我现在养成的习惯是任何类相关的疑难杂症第一轮先跑一条sc -d和一条sm -d把类加载器和方法签名截图留底再往下走。这样做能省掉大量的试错时间而且事后复盘时这些截图就是最扎实的证据。如果你在排查中遇到 NoSuchMethodError、ClassNotFoundException、或者方法明明存在却调用不了的问题不妨先冷静下来敲一遍这两个命令。它们没有 watch 那么花哨但往往直击要害。搞清楚“加载的是哪份代码、方法签名到底长什么样”离找到问题根源就已经不远了。