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

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

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

一、找到 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=10000freightCents=800freeShipping=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 / 业务进程,输入对应序号回车。

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

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.x3.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 止血?最后有没有补一版正式发版把修复固化下来?

相关推荐
名字还没想好☜1 小时前
Go 结构体内存对齐:调整字段顺序,同样的字段省下 40% 内存
开发语言·后端·golang·go·内存对齐
颜进强1 小时前
从零搭建私人 RAG 实战:用 Markdown 沉淀技术决策与业务决策
前端·后端·ai编程
用户37899822121281 小时前
别再「凭感觉」写代码了:我用 Qoder 花一天时间,从 0 到 1 真正掌握了 Vibe Coding(附完整踩坑实录)
后端
玛丽莲茼蒿1 小时前
很难出错的Spring5(十)—— IOC进阶
java·开发语言·jvm
码农进化录2 小时前
Java 程序员的 AI 进化论 | AI 加 Postman 跑接口测试,省了三天活
java·后端·openai
旺仔学长 哈哈2 小时前
基于SpringBoot的在线招聘测评系统的设计与实现----附源码35253+数据库文档
数据库·spring boot·后端·在线招聘
花生了什么事o2 小时前
JVM运行时数据区详解:变量、对象和类信息到底存在哪里
jvm
Ivanqhz3 小时前
Rust #[derive(Serialize)]浅析
开发语言·后端·rust
卷无止境3 小时前
Python的魔术方法:那些藏在双下划线背后的魔法
后端·python