上篇回顾Day 83 用 DDD 限界上下文把复杂业务边界切清楚、代码物理隔离新人三天能上手。但架构再清晰运行起来也可能半夜崩——而生产机器默认不开 debug 端口jstack 都用不了。这篇把 Arthas 这把凌晨救命手术刀讲透。生产诊断的第一个现实是很多 Spring Boot 服务默认不开 JMX、不开 debug 端口IDEA Debug、VisualVM、JProfiler 这些开发环境手段远程全用不了。top -Hp pid看到线程吃满 CPU接下来却无从下手——重启要 20 分钟、回滚 40 分钟赶上客户投诉就是 P0。Arthas 是阿里开源的 Java 在线诊断工具能在不重启进程、不改代码的情况下 attach 到运行中的 JVM 上做手术thread -n 3一行定位吃 CPU 的线程、jad反编译看实现、watch监控调用栈——五分钟定位、十五分钟改完走紧急发布全程不重启服务。今天把这套工具真正用进生产。一、为什么 JVM 自带工具不够用很多新人以为线上诊断靠jstack、jmap、jstat这三板斧就够了。确实够用但不好用——它们的痛点有三看到线程名却看不到方法jstack 输出是线程堆栈但代码经过多层框架封装从AbstractHandlerMethodAdapter.invoke这种方法名里看不出是谁调的业务方法dump 太重jmap -dump 一执行几十秒到几分钟内进程几乎停摆对线上是二次打击改不动代码发现问题想立刻修对不起重启走流程发版回滚搞定是 30 分钟后的事了Arthas 解决的就是这些。它通过 Java Agent 机制 attach 到运行中的 JVM 上提供 100 命令常见诊断需求都能在不重启的情况下完成。核心依赖如下!-- pom.xml只引入 arthas-core让 attach 启动器自动下载最新版本 -- dependency groupIdcom.taobao.arthas/groupId artifactIdarthas-spring-boot-starter/artifactId version4.0.5/version /dependency启动后默认监听 3658 端口你可以通过http://localhost:3658/在浏览器里看交互式控制台也可以用curl远程调用生产配合 VPN/Spring Boot Actuator 做权限管控。二、三板斧实战watch / trace / thread这三板斧是 Arthas 最常用的命令对应看方法被调用情况 / 看调用链路 / 看线程状态。组合使用能解决 80% 线上问题。2.1 thread 命令5 秒定位 CPU 飙高根因CPU 飙高第一步哪个线程吃满了Arthas 的thread -n 3直接给你 CPU 占用前 3 的线程堆栈# attach 到进程后在 arthas 控制台里执行 $ thread -n 3 http-nio-8080-exec-127 Id237 cpuUsage89.7% deltaTime1248ms time8923ms at com.example.order.service.OrderService.calculateCartesianCoupon(Ljava/util/List;)V at com.example.order.service.OrderService.applyBestCoupon(OrderDTO) at com.example.order.service.OrderService.createOrder(OrderDTO) ... ​ GC-thread-7 Id87 cpuUsage12.3% ... http-nio-8080-exec-130 Id240 cpuUsage8.1% ...看到没直接给出业务方法名calculateCartesianCoupon——笛卡尔积优惠券计算一下子就知道哪个方法出问题了。更进一步定位到具体方法后你想知道这个方法是哪个用户请求触发的参数是啥——用trace命令# 监控 calculateCartesianCoupon 方法的调用耗时与方法入参 $ trace com.example.order.service.OrderService calculateCartesianCoupon \ #cost 50 \ -n 5 ​ Press Q or CtrlC to abort. Affect(class-cnt:1 , method-cnt:1) cost in 87 ms. ---ts2026-08-30 02:14:23;thread_namehttp-nio-8080-exec-127;id237;is_daemontrue;priority5;TCCL... ---[39.3ms] com.example.order.service.OrderService:calculateCartesianCoupon() ---[2.1ms] queryUserCoupons() #38 # 第一次 SQL 查询 ---[37.2ms] cartesianProduct() #52 # 罪魁祸首笛卡尔积 ---[0.0ms] filterBestCoupon() #67输出里thread_namecost都有了配合 Spring 的MDC日志里加 traceId你就能精准关联到日志系统看具体哪个用户 / 哪个请求 / 触发原因。生产中这一套链非常值钱。想看调用栈的上下文——方法被谁调、调用方传了啥参数——用stack# 看 calculateCartesianCoupon 被谁调用、调用栈完整链路 $ stack com.example.order.service.OrderService calculateCartesianCoupon \ -n 3 \ params[0].size 50 # 只看商品/优惠券超过 50 个的请求 ​ ts02:18:11;thread_namehttp-nio-8080-exec-127;... OrderController.createOrder() OrderService.applyBestCoupon() OrderService.calculateCartesianCoupon()2.2 watch 命令实时监控方法入参返回值trace 帮你看耗时watch 帮你看数据——调用一次方法看它实际接收了什么参数、返回了什么结果。这是排查参数错乱返参被改MQ 消费假阴性问题的利器# 监控 MQ 消费者每次消费消息的 payload 与处理时长 $ watch com.example.mq.OrderConsumer onMessage \ {params, returnObj, throwExp, cost} \ -x 2 \ -f # -f 表示方法结束后再 watch-n 限制次数默认无限 ​ Press Q or CtrlC to abort. Affect(class-cnt:1 , method-cnt:1) cost in 56 ms. ts2026-08-30 02:24:33; cost1245ms; paramsJSON[ {orderId:202608300001,amount:89.50,couponId:[C001,C002,C003]} ] returnObjnull # 注意返回 null消费失败 throwExporg.springframework.dao.DataIntegrityViolationException这个输出瞬间告诉你三件事消息内容是什么、为什么报错、错误类型是啥——拿到这些信息你甚至不用看代码就能猜到是优惠券 ID 重复导致唯一索引冲突。2.3 反编译 上下文jad 命令的杀手用法watch/trace 都给出了方法但你看不懂业务逻辑用jad反编译生产代码方法级实时看# 反编译 OrderService 类完整源码默认输出到 console $ jad com.example.order.service.OrderService ​ ClassLoader: -org.springframework.boot.loader.LaunchedURLClassLoader17f4dba -sun.misc.Launcher$AppClassLoader4b9af9 ​ Location: /BOOT-INF/classes/com/example/order/service/OrderService.class ​ public OrderService createOrder(OrderDTO order) { // 反编译拿到的真实代码片段 ListCoupon coupons couponService.queryByUser(order.getUserId()); if (CollectionUtils.isEmpty(coupons)) return null; return calculateCartesianCoupon(coupons); // 看就是这一行 }配合watch时加上-x 2能展开两层参数对象配合 jad 出的代码你就像在本地 IDE 单步调试一样看清问题。这是 jstack 永远做不到的事情——因为 jstack 只能告诉你栈帧在哪个方法不能告诉你那个方法现在长什么样。三、死锁排查thread -b 一行定位死锁是最难复现的问题之一——开发环境一切正常线上偶发雪崩后大家看日志里没有 ERROR 也找不到头绪。Arthas 的thread -b直接给你当前正在等待锁的线程链$ thread -b http-nio-8080-exec-127 Id237 BLOCKED on java.util.concurrent.locks.ReentrantLock1f3e2a1 held by: http-nio-8080-exec-130 Id240 at com.example.order.service.InventoryService.lockStock(InventoryService.java:85) - waiting to lock 0x00000006c1f3a1b0 (a ReentrantLock) ... ​ http-nio-8080-exec-130 Id240 BLOCKED on java.util.concurrent.locks.ReentrantLock1f3e2b2 held by: http-nio-8080-exec-127 Id237 at com.example.order.service.CouponService.markUsed(CouponService.java:42) - waiting to lock 0x00000006c1f3a0a0 (a ReentrantLock) ...一眼看出线程 127 和 130 互相等对方的锁——经典的 A 等 B、B 等 A 环。配合jad反编译两个类的方法定位到锁顺序不一致的代码段。生产实践要点先用thread看是否真有大量 BLOCKED 线程10 个就该警觉thread --state BLOCKED单独看所有阻塞线程的分布thread -b只对当前活跃的死锁链有效如果死锁已经解开就看不到了所以告警收到要立刻 attach四、内存泄漏排查dashboard vmtool 黄金组合CPU 飙高有 watch/trace 治死锁有 thread -b 治最难的是内存泄漏——它可能是几个小时内慢慢爬升直到老年代被打满触发 Full GC 才告警。第一步用dashboard命令看整体内存水位$ dashboard ID NAME GROUP PRIORI STATE %CPU TIME INTERRUP DAEMON 237 http-nio-8080-exec-127 main 5 RUNNABLE 68.7% 12:48 false true ... Memory used total max usage GC heap 3456M 4096M 4096M 84.36% gc.ps_mark... ps_eden_space 512M 640M - 80.00% gc.ps_scavenge.count 1247 ps_old_gen 2800M 2816M - 99.43% gc.ps_marksweep.count 24 -- 老年代 99.43% 满了 non_heap 420M -1 -1 89.83% code_cache ...老年代 99.43%明显异常。第二步用vmtool看具体哪个类的实例数最多# vmtool 是 4.0 的新命令功能类似 jmap -histo 但能远程调用 $ vmtool --action getInstances \ --className com.example.order.domain.Order \ --limit 10 \ --express instances.{#this.getClass().getSimpleName() : #this.id} ​ Order[][ Order[idORD20260830001234,userIdU8821,amount120.5], Order[idORD20260830001233,userIdU7712,amount89.0], ... ]配合 Java 业务侧的可疑类监控代码可以很快锁定内存膨胀的对象// MemoryLeakDetector.java — JDK 17 Spring Boot 3.3 // 周期性输出可疑的内存膨胀对象配合 vmtool 加快排查 Component Slf4j public class MemoryLeakDetector { ​ private final MeterRegistry registry; private final AtomicLong orderCacheSize new AtomicLong(0); private final AtomicLong unfinishedOrderCount new AtomicLong(0); ​ // 提供一个 ConcurrentHashMap 模拟问题代码实际工作里可能是 Spring Cache / Session private final ConcurrentHashMapString, Order suspiciousCache new ConcurrentHashMap(); ​ PostConstruct public void startMonitor() { // 每 30 秒打印一次可疑对象大小 Executors.newSingleThreadScheduledExecutor() .scheduleAtFixedRate(this::sampleMemory, 30, 30, TimeUnit.SECONDS); } ​ // 业务方法里调用实际排查时要找到这个类的位置 public void putToCache(String key, Order value) { suspiciousCache.put(key, value); orderCacheSize.incrementAndGet(); } ​ private void sampleMemory() { // 1. 报警阈值超过 5000 个订单就告警 int size suspiciousCache.size(); if (size 5000) { log.warn([内存监控] suspiciousCache.size{}, 可能存在内存泄漏, size); } ​ // 2. 上报 Micrometer 指标方便 Grafana 看板查看趋势 registry.gauge(cache.suspicious.order.size, Tags.of(type, order), suspiciousCache, Map::size); // 3. 触发 dump 阈值——老年代使用 90% 时主动 dump heap MemoryUsage oldGen ManagementFactory.getMemoryMXBean() .getNonHeapMemoryUsage(); long usedBytes ManagementFactory.getMemoryMXBean() .getHeapMemoryUsage().getUsed(); long maxBytes ManagementFactory.getMemoryMXBean() .getHeapMemoryUsage().getMax(); if (maxBytes 0 (double) usedBytes / maxBytes 0.90) { log.error([内存告警] 老年代使用率 {}%, 触发自动 dump, (double) usedBytes / maxBytes * 100); // dump 到 /tmp/heapdump-{时间戳}.hprof triggerHeapDump(); } } ​ private void triggerHeapDump() { // 用 HotSpotDiagnosticMXBean 触发主动 dump try { MBeanServer mbs ManagementFactory.getPlatformMBeanServer(); HotSpotDiagnosticMXBean hotSpot ManagementFactory.newPlatformMXBeanProxy( mbs, com.sun.management:typeHotSpotDiagnostic, HotSpotDiagnosticMXBean.class); hotSpot.dumpHeap(/tmp/heapdump- System.currentTimeMillis() .hprof, true); } catch (Exception e) { log.error(dump heap 失败, e); } } }上面这个类有两个用法线上埋点把可疑的缓存对象如 SpringCacheable装饰的方法的 size 上报到 Prometheus触发 dump老年代使用 90% 时主动 dump配合dashboard输出你就不用手动盯了拿到 dump 文件后用 MATMemory Analyzer Tool分析 Leak Suspects 报告看哪些对象持有 GC Root——这是教科书式的内存泄漏排查流程。五、热修复jad mc redefine 不停机改 Bug诊断完了是修复。但生产环境重启一次要 20 分钟回滚兜底又要 40 分钟——这个窗口里服务可用性直接掉 5 个点。Arthas 提供不停机热修复能力三步搞定# 1. 反编译生产代码jad $ jad --source-only com.example.order.service.OrderService \ /tmp/OrderService.java ​ # 2. 在反编译出来的代码里改一行——比如把 NPE 风险的地方加上空判断 $ vim /tmp/OrderService.java # 修改 calculateCartesianCoupon 方法 # 原来return cartesianProduct(coupons); # 改成if (CollectionUtils.isEmpty(coupons)) return Collections.emptyList(); # return cartesianProduct(coupons); ​ # 3. 用 Arthas 自带的内存编译器mc编译 $ mc -d /tmp /tmp/OrderService.java Memory compiler output: /tmp/com/example/order/service/OrderService.class ​ # 4. 用 redefine 把新 class 加载到运行中的 JVM 里替换原 class $ redefine /tmp/com/example/order/service/OrderService.class redefine success, size: 1整个过程 5 分钟搞定业务线程零中断——下个请求来的时候OrderService就已经是新逻辑了。但这条路有三道魔鬼细节坑表现解决方案反编译丢失注释改完的代码看起来不一样jad --source-only 只输出方法体手动加 importmc 编译失败提示缺 jar加--classLoaderClass参数指定 Spring Boot ClassLoaderredefine 不生效接口增删方法做不到redefine只能修改方法体不能增减字段/方法/注解ClassLoader 隔离多个同名类不在一个 ClassLoader用sc -d com.example.order.service.OrderService看真实 ClassLoader重要限制redefine 不是万能的。你不能新增字段、不能改方法签名、不能改注解。能改的只有方法体内的语句。大型功能修复就老老实实走发版流程热修复适合小修小补场景——比如修个 NPE、调整下单限流阈值、加段日志。六、和 JProfiler / async-profiler 的组合拳Arthas 的短板是火焰图——trace 命令虽然能看出方法耗时但不像火焰图那样直观看到调用链路上哪一段占大头。生产里我通常是这么组合的Arthas 定位找到问题线程 / 问题方法 / 问题入参async-profiler 出火焰图看完整的 CPU 时间分布JProfiler 出内存用 Heap Walker 看对象引用链async-profiler 接入很简单# 启动 async-profilerattach 到进程 ID $ ./profiler.sh -d 30 -f /tmp/flamegraph.html pid # 30 秒采样输出 HTML 火焰图 # 用浏览器打开查看 CPU 时间分布进阶用法把 async-profiler 通过 Arthas 的profiler命令直接启动无须外部工具$ profiler start --event cpu --interval 10000000 # 10us 采样 $ profiler stop --format html --file /tmp/arthas-flame.html火焰图里平顶山就是 CPU 瓶颈的根因——你看那一块调用链越平、越宽就是热点代码段。我们上次生产 OOM最终定位到这个调用... computeCartesianProduct [95.3%] - hashSet.contains() [42.1%] - list.toArray() [15.6%]找到是用 HashSet 反复 contains 判断有 42% 的时间花在 hashCode 计算上——这一查发现Order#hashCode里包含了一个读数据库的字段。修复方案把Order的内存视图和持久化视图分开问题立刻解决。整个排查到修复 1 小时搞定。七、建议这几条是从无数次凌晨两点告警里总结出来的生产环境预装 Arthas**不要等出问题了再装 arthas-spring-boot-starter。把它加进基础镜像exclusions排除 arthas-boot避免污染主应用启动通过环境变量ARTHAS_ENABLEDtrue控制启动。出问题直接 attach别在凌晨两点 clone 代码打包。敏感信息红线Arthas 的jad命令能看到所有业务代码包括注释、字段值。生产环境部署时一定要做访问控制或者封装到 Spring Boot Actuator 后只对内网 VPN 暴露或者用arthas.properties配白名单 IP。别拿裸 Arthas 端口直接挂公网**——这是生产事故的常见导火索。热修复不是银弹把 Arthas 热修复当作止血工具——临时规避问题、降低影响面真正的解决方案还是走代码评审 自动化测试 灰度发布。把热修复代码当天**沉淀成 P0 issue 跟进避免先用 redefine 顶着回头再修变成三个月都没修。下一篇 Day85我们要解决线上容量评估问题——你怎么在不影响生产的情况下验证你的服务能扛住双十一的 10 倍流量JMeter 压测平台怎么搭影子库影子表怎么搞压测报告怎么写才不被领导挑刺这些问题我们下篇见。Arthas 不是用来替代代码审查和单元测试的而是用来在代码还没修好时让你也能在生产环境活下来的工具。每个 Java 后端工程师的电脑里都应该有一份——不是因为你天天用而是因为你永远不知道凌晨两点它能救你一命。