Arthas mc + retransform 实战:线上改完代码不用重新发版

大家好,我是程序员天天困。

这是「Arthas 线上诊断实战」系列第 4 篇,专门讲 Arthas mc + retransform:JVM 还活着时,把写错的方法体当场换掉。前三篇 watch、trace、vmtool 负责找问题,这篇负责止血。点个收藏,我们直接上手。

一、找到 bug 之后:发版流水线才是真瓶颈

线上最折磨人的,往往不是「不知道哪错了」,而是「知道了也要等一整轮发布」。

有人就摊上过这种事:免运费订单被多扣了一笔运费,watch 一眼就钉死在计价方法里。改动其实就一行,但测试、审批、灰度排期排下来,现场还得先人工兜底。那种感觉特别像家里保险丝烧了,电工师傅明明带了备用熔丝,却非要等物业把整栋楼断电检修------你懂有多离谱。

所以问题变成:能不能在不重启、不整包发版的前提下,把已经加载的类里那段错误逻辑换掉?能。Arthas 给的组合拳就是 mc + retransform。

二、Arthas mc 是什么:内存里把 .java 编成 .class

Arthas mc 解决的不是「会不会写代码」,而是「在目标进程的 classpath 环境下,把修正后的源码编成 class」。

Arthas mc(Memory Compiler / 内存编译器) :Arthas 内置命令,用来把 .java 文件编译成 .class。你可以把它理解成「站在目标 JVM 旁边的便携 javac」------难的是对齐依赖和 ClassLoader,不是敲编译命令本身。

官方 Arthas mc 文档 写得很直白:编译出 class 之后,再结合 retransform 做热更新。

系列还是用 com.ttk 包。假设有个优惠券计价服务,免运费分支写错了------明明免运费,结果还把运费加了进去:

java 复制代码
@Service
public class CouponPriceService {
    // 示例:金额单位用「分」,避免浮点
    public int calcPayCents(int goodsCents, int freightCents, boolean freeShipping) {
        if (freeShipping) {
            // bug:免运费却仍加上了运费
            return goodsCents + freightCents;
        }
        return goodsCents + freightCents;
    }
}

请求 goodsCents=10000、freightCents=800、freeShipping=true,应付应该是 10000,实际却吐 10800。watch 已经确认入参没错、就是方法体写错了。接下来不是再开一轮发版,而是走完整热更新链路:改源码 → 启动 Arthas → mc 编译 → retransform 替换。

三、实战全流程:改源码 → 启动 Arthas → mc 编译

热更新不是一上来就敲 mc,得先有正确源码,再把 Arthas Attach 到目标进程。

1)先改方法体,别动类结构

把免运费分支改对,其它签名、字段一个字都别动:

java 复制代码
@Service
public class CouponPriceService {
    public int calcPayCents(int goodsCents, int freightCents, boolean freeShipping) {
        if (freeShipping) {
            return goodsCents;
        }
        return goodsCents + freightCents;
    }
}

源码路径是:src/main/java/com/ttk/service/CouponPriceService.java。保存文件就行,先别重启应用------我们要验证的是「不重启也能换逻辑」。

本地演示直接在项目根目录改源码就行。若是远端机器,把改好的 .java 拷到服务器(和 arthas-boot.jar 放一块最省事),mc 命令里填实际路径。

2)启动 Arthas,Attach 到业务进程

mc / retransform 都是 Arthas 控制台里的命令,必须先把诊断 agent 挂到目标 JVM 上。没装过的话,先拉 boot 包:

bash 复制代码
curl -O https://arthas.aliyun.com/arthas-boot.jar

已经下过 jar 的话,进放 jar 的目录直接启动:

bash 复制代码
java -jar arthas-boot.jar

启动后,Arthas 会列出本机 Java 进程(带序号)。找到你的 Spring Boot / 业务进程,输入对应序号回车。

看到如图这种效果,才说明你已经「进到」目标进程里了,后面的 mc、retransform、jad 都在这个交互里敲。

3)在 Arthas 里执行 mc

确认提示符已经是 [arthas@PID]$ 之后,在 mc 命令里写源码相对项目根目录的路径(写绝对路径也行):

bash 复制代码
mc src/main/java/com/ttk/service/CouponPriceService.java -d target/classes

拆开看三点:

  1. mc:走内存编译,产物是 .class。
  2. 源码路径:给 .java,不是 class;这里直接指向 src/main/java/com/ttk/service/CouponPriceService.java。
  3. -d target/classes:把 class 打到 Maven 默认输出目录;不写的话,终端里会打印实际路径,照着抄就行。

Spring Boot fat jar / 多 ClassLoader 时,若提示找不到依赖类,可按官方补 -c <classLoaderHash>(hash 用 sc -d <类全名> 拿,看输出里的 classLoaderHash 字段),或:

bash 复制代码
mc --classLoaderClass org.springframework.boot.loader.LaunchedURLClassLoader \
  src/main/java/com/ttk/service/CouponPriceService.java -d target/classes

注:上面的 LaunchedURLClassLoader 全限定名对应 Spring Boot 2.x ;3.2+ 改到了 org.springframework.boot.loader.launch.LaunchedURLClassLoader,按实际版本调整。

编译成功时,终端会打印 class 输出路径。你自己截一张:

编译成功只说明「源码能编过」,还不等于线上逻辑已换------下一步还是在同一个 Arthas 会话里,把 class 塞进正在跑的 JVM。

四、Arthas retransform:把 class 热塞进已加载的类

JVM 允许部分更新已加载的类,但只愿意改「方法体这一层」,不愿意你改类的骨架。

retransform(类字节码热替换) :Arthas 命令,加载外部 .class,对 JVM 里已经加载的同名类做 retransform。底层对应 JDK 的 Instrumentation#retransformClasses。你可以把它想成「给正在跑的类换一块新的方法实现芯片」,插座形状(字段、方法签名)必须原样。

用法很直接,还在刚才那个 [arthas@PID]$ 会话里,把上一节编出来的 class 路径贴进去:

bash 复制代码
retransform target/classes/com/ttk/service/CouponPriceService.class

成功时一般会看到类似 retransform success,并打印受影响的类名。你自己截一张:

官方明确写了几条硬限制,日常最常踩的是这些:

你想做的事 retransform 能不能干 我的建议
只改方法内部逻辑 能 这是热更新主战场
新增 / 删除 field、method 不能 老实走发版
改方法名、字段名 不能 等于改结构,别硬刚
正在跑、还没退出的那次调用立刻换逻辑 通常不行 等这次调用跑完,下一次再生效

为什么 schema 一点都不能动? 类一旦被 JVM 加载,字段的内存偏移、方法表(vtable)这些布局就固化了,堆上现存的实例都按这套布局摆放。这时如果允许加字段、加方法,老实例的内存就对不上新的 schema------所以 JVMTI 规范直接禁止 schema 变更,只允许换方法体字节码。

说白了:日常线上 80% 的热修,本来就是「函数里算错了」,不是「函数少写了」。只要你守住「只动方法体」,retransform 就够用。

可能有人会问:类早就加载了,JVM 还在跑,真的能更新吗?

能,但只是「部分更新」。JDK 的 Instrumentation 支持 retransformClasses;新增字段、新增方法、改签名这类结构变动会被拒绝。另外要注意:已经进入方法栈的那次调用会继续跑旧字节码,等它返回后,下次进入这个方法才走新逻辑------这就是表格里「通常不行」的原因。别把热更新当成随意改类的万能补丁。

官方还提供 retransform -l 看 entry、-d / --deleteAll 清理 entry。想消除影响,用 retransform -d <id> 删掉对应 entry 即可------Arthas 会把类还原到热替换之前的状态。注意:Arthas stop 不会自动还原 retransform 过的类,必须手动清理------这点我第一次踩到时挺懵的,后来才养成「修完就清 entry」的习惯。

五、怎么确认生效:业务结果 + jad 反编译

热更新有没有生效,最终要以业务结果为准,jad 只是加一层「源码证据」。

再发一笔免运费订单:应付金额应从 10800 回到 10000。对了,基本就成了。想看字节码层是不是已换,用 jad:

bash 复制代码
jad --source-only com.ttk.service.CouponPriceService

重点盯 freeShipping 分支:应只剩 return goodsCents;,不再偷偷加运费。截图对照一下:

jad(Just Another Decompiler):Arthas 的反编译命令,把运行中的 class 还原成接近源码的 Java。你可以把它当成「对着活着的字节码拍照」。

没源码、只有依赖 jar 出问题的时候,常见闭环是:jad 拉出源码 → 本地改方法体 → mc 编译 → retransform 打回去。官方也把这条链路写进了 retransform 文档。

六、Arthas mc 编译失败怎么办,以及线上安全边界

mc 失败很常见,别死磕远端编译;生产热更新永远是止血手段,不是正式发版的替代品。

依赖对不齐时,终端会甩一堆 class not found 之类的报错。

我自己的习惯是:

  1. 优先在本地 IDE / CI 把 class 编好,上传到机器,直接 retransform。
  2. 机器禁止直接传文件时,可按常见运维做法:本地 base64 编码 class → 粘到远端文本 → 再 decode 还原,并用 md5 校验。
  3. 真要在远端 mc,再考虑 -c / --classLoaderClass 对齐 ClassLoader------这条路径麻烦,我很少首选。

还有个版本坑要单独提:编译用的 JDK 版本不能高于目标 JVM 。本地用 JDK 17 编出来、目标 JVM 是 JDK 8,class 文件主版本号是 61,retransform 会直接抛 UnsupportedClassVersionError。mc 在远端编译时也会踩这个坑------它走的是 Arthas 启动时所在 JDK 的 javac,所以最好让 Arthas 用和业务进程同版本的 JDK 启动。

还有几条我建议写进团队约定:

  • 先在预发 / 测试机完整走一遍 mc + retransform + jad,再碰生产。
  • 热更新成功 ≠ 永久修复。机器一重启,还是旧包;正式修复必须补一版落盘代码。
  • 改的是线上行为,没有评审、没有回滚预案就别上。止血可以,当常规发版通道不行。

七、watch / trace / vmtool / mc+retransform 怎么搭配

四期写下来,口诀就一句:先看清,再热修;热修只动方法体,修完仍要补发版。

阶段 优先命令 你在解决什么
入参 / 返回对不对 watch 结果对错
走了哪条分支、哪层慢 trace 路径与耗时
没请求也要看对象状态 vmtool 堆内实例
已定位到方法体写错 mc + retransform 在线换逻辑
确认字节码是否已换 jad 反编译核对

推荐顺序:能复现就 watch/trace → 要看状态就 vmtool → 确认是方法体逻辑错误再用 Arthas mc + retransform → jad 验收 → 另开正式发版把修复固化进仓库。

结语

Arthas mc 负责把修正后的 .java 编成 class,retransform 负责把它热塞进还在跑的 JVM。会用这两步,再加上 jad 验收和对「只能改方法体」的边界感,你就多了一条「发现 bug 后立刻止血」的路,而不必每次都等一整轮发版。

下一篇我会写这个系列的 Profiler :专门对付「CPU 热点到底在哪、火焰图怎么读」这类问题。本文参数以 Arthas mc 官方文档 与 retransform 官方文档 为准,版本变了以官网为准。


我是程序员天天困,持续分享编程干货。觉得有用的话记得点赞收藏和关注~也欢迎在评论区聊聊:你有没有在生产上用过 Arthas mc + retransform 止血?最后有没有补一版正式发版把修复固化下来?

相关推荐
金銀銅鐵6 小时前
[Java] 用 GUI 展示 EnumSet 增减元素时的内部变化(上)
java·后端·ai编程
念何架构之路6 小时前
go-grpc设计思想
开发语言·后端·golang
system_error_x7 小时前
SpringBoot 集成 Thymeleaf 简洁入门
java·spring boot·后端
步行cgn8 小时前
Spring MVC 视图机制的核心接口与类
java·开发语言·后端·spring·mvc
码事漫谈9 小时前
AI 读你的提示词,其实很没耐心
后端
用户83562907805110 小时前
如何使用 Python 格式化 Excel 单元格中的长文本
后端·python
我命由我1234510 小时前
Python 开发 - Python 的 pathlib
java·开发语言·后端·python·学习·java-ee·学习方法
IT_陈寒10 小时前
Java性能优化完全指南
前端·人工智能·后端
电子IT笔记10 小时前
对接群晖NAS,上传和删除踩坑记录
前端·后端
Mk5987810 小时前
P4:把 Agent 的记忆搬进 Redis —— AgentScope-Java 工程化实战
后端