主题
界面
主题
《Arthas 与线上问题诊断》 说「Arthas = attach + 字节码增强」——本节把这句话亲手实现出来。
★ 本节先读字节码,再亲手写一个能把方法「改写」的 Agent,两条路全部实测:
静态 -javaagent:[agent] 已改写 …AgentTarget:1679 → 2072 字节 ✓✓(类加载时改写)
动态 attach :前 4 次调用「裸奔」→ attach → 改写 → 第 5 次起每次多出耗时日志 ✓✓✓★★ 还踩了两个真实深坑并解决(比顺风顺水更有价值): 主类先于 agent 被加载(必须重转换)+ 重转换禁止新增方法(REBASE 策略被 JVM 拒绝)。
javap -c 读基础字节码指令(栈操作、方法调用、异常)★★★;premain 与 agentmain ★★★;★ 你的代码「跑起来」和「你写的样子」之间,隔着一层字节码 ✓✓:
| 你以为 ✗ | 实际 ✓ |
|---|---|
字符串拼接就是 StringBuilder | JDK 9+ 是 invokedynamic 的 makeConcatWithConstants ✓ |
Integer i = 1 没有调用 | 编译器插了 Integer.valueOf(缓存 -128~127)✓ |
| 性能工具是「黑魔法」 | 都是 agent + 字节码增强(JaCoCo 探针、Arthas、APM)✓✓ |
★ 会读字节码 = 排查 NoSuchMethodError、反编译对不上、AOP 不生效时不再抓瞎 ✓✓✓。
javap -c -p -cp <classes> cn.kb.learn.deep.jvm.BytecodeSample★ 四段实测输出「看什么」 ✓✓✓:
public int add(int, int); ← 方法签名
0: iload_1 ← 把第 1 个参数压栈(实例方法:0 号是 this)
1: iload_2 ← 把第 2 个参数压栈
2: iadd ← 弹出两个、相加、压回
3: ireturn ← 返回 int
public java.lang.String concat(String, int);
2: invokedynamic … makeConcatWithConstants ← 字符串拼接的「现代实现」✓
public java.lang.Integer boxed(int);
1: invokestatic … Integer.valueOf ← 自动装箱的真相 ✓
public int guard(int);
1: ifge 14 ← 条件跳转(>= 0 就跳到 14)
4: new … IllegalArgumentException
13: athrow ← 抛出异常
14: iload_1 / 15: iconst_2 / 16: imul / 17: ireturn ← value * 2★★ 三条常用指令 ✓✓:iload/istore(读写局部变量)、iadd/imul(运算)、 invokestatic/invokespecial/invokevirtual/invokedynamic(调用)、athrow(抛异常)。
public static void premain(String args, Instrumentation inst) // -javaagent:xxx.jar 启动时挂载
public static void agentmain(String args, Instrumentation inst) // 运行时 attach 挂载(Arthas 的路)✓✓★ 三个必备条件 ✓:
META-INF/MANIFEST.MF 写明 Premain-Class / Agent-Class(本节用 maven-jar-plugin 写入)✓;Can-Retransform-Classes: true ✓;inst.addTransformer(new ClassFileTransformer() {
public byte[] transform(ClassLoader l, String className, Class<?> beingRedefined,
ProtectionDomain pd, byte[] classfileBuffer) {
// 你收到「旧字节码」,返回「新字节码」—— 就这么简单
}
}, true); // canRetransform=true★★ 两种时机 ✓✓✓:
| 时机 | 何时发生 | beingRedefined |
|---|---|---|
| 加载时改写 | 类被加载那一刻 | null |
| 重转换(retransform) | 类已加载,事后改写 | 非 null(attach 场景) |
★ 实战写法(本节采用):手写 transformer + 用 ByteBuddy 做「方法内联」—— 先取 buffer 当原始类、再 Advice 内联、再返回新数组 ✓✓,全程可控可见:
[agent] 已改写 cn.kb.learn.deep.jvm.agent.AgentTarget:1679 → 2072 字节 ✓✓★ 坑 1:主类可能先于 agent 被加载 ✗✗
只 addTransformer 不重转换 → 运行 10 次调用,一次增强都没生效 ✗✗
(main 类在 premain 之后就绪之前已进入加载流程)
正解:安装 transformer 后,对已加载的目标类主动 inst.retransformClasses(loaded) ✓✓✓★ 坑 2:重转换禁止新增方法 ✗✗✗(本节真实报错)
[agent] retransform 失败:java.lang.UnsupportedOperationException:
class redefinition failed: attempted to add a method原因是默认的 REBASE 策略会新增辅助方法来搬移原方法体;而 JVM 规定: 重定义只能改方法体,不能增删方法/字段 ✗✗。 正解:用 REDEFINE 思路(只改方法体) ✓✓✓ —— 本节最终版的手写 transformer 走的正是这条路。
★★ 这两条坑解释了一个常见困惑:为什么「动态开的功能」总比「启动参数」限制多 —— attach 场景 = 在「已经跑起来的类」上做手术 ✓✓。
| 工具 | 用途 |
|---|---|
| JaCoCo | 给类插探针统计覆盖率(T1-004、T1-018 的门禁数据来源)✓✓ |
| Arthas / APM | 在线诊断与监控(《Arthas 与线上问题诊断》)✓✓ |
| Mockito | 运行时生成 mock 类 ✓ |
| 热部署工具 | 改代码不重启,靠重转换 ✓ |
javap -c -p -cp java-deep/target/classes cn.kb.learn.deep.jvm.BytecodeSamplecd labs
.\mvnw.cmd -B -ntp -pl java-deep package -DskipTests
# 拿到 ByteBuddy 的 jar 路径(agent 需要它做字节码改写)
.\mvnw.cmd -B -ntp -q -pl java-deep dependency:build-classpath "-Dmdep.outputFile=target/cp.txt"
$bb = (Get-Content java-deep/target/cp.txt -Raw -Encoding UTF8).Trim()
java "-javaagent:java-deep/target/java-deep-1.0.0.jar" -cp "java-deep/target/classes;$bb" `
cn.kb.learn.deep.jvm.agent.AgentTargetjava -cp "java-deep/target/classes;$bb" "-Dkb.agent.jar=$((Resolve-Path java-deep/target/java-deep-1.0.0.jar).Path)" `
cn.kb.learn.deep.jvm.agent.AgentAttachLesson★ attach 的三步 ✓✓✓: VirtualMachine.attach(pid) → loadAgent(jarPath) → detach() —— 目标进程收到 agentmain 回调,之后就是标准的字节码增强 ✓。
代码:labs/java-deep/src/main/java/cn/kb/learn/deep/jvm/BytecodeSample.java + labs/java-deep/src/main/java/cn/kb/learn/deep/jvm/agent/{TimingAgent,TimingAdvice,AgentTarget,AgentAttachLesson}.java
实测一(javap,节选):
public int add(int, int);
0: iload_1
1: iload_2
2: iadd
3: ireturn
public java.lang.String concat(java.lang.String, int);
2: invokedynamic #7, 0 // InvokeDynamic #0:makeConcatWithConstants:(…)Ljava/lang/String;
public java.lang.Integer boxed(int);
1: invokestatic #11 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;实测二(静态 -javaagent):
[agent] TimingAgent 已挂载(静态挂载 -javaagent)
[agent] 已改写 cn.kb.learn.deep.jvm.agent.AgentTarget:1679 → 2072 字节
AgentTarget 启动,pid=12248
[agent] slow 耗时 123.2 ms
第 1 次调用 slow(1) → done-1
[agent] slow 耗时 120.5 ms
第 2 次调用 slow(2) → done-2
…实测三(动态 attach)—— 「裸奔 → 手术 → 生效」全过程 ✓✓✓:
== 1. 启动目标 JVM(此时没有 agent,前几次调用是「裸奔」的)==
AgentTarget 启动,pid=3416
第 1 次调用 slow(1) → done-1 ← 裸奔
第 2 次调用 slow(2) → done-2 ← 裸奔
== 2. attach 到 pid=3416 并 loadAgent ==
[agent] TimingAgent 已挂载(动态 attach)
第 3 次调用 slow(3) → done-3
第 4 次调用 slow(4) → done-4
[agent] 已改写 cn.kb.learn.deep.jvm.agent.AgentTarget:1679 → 2072 字节 ← 手术完成
[agent] 目标类已加载,重转换完成
== 3. agent 已加载,观察后半段输出的耗时日志 ==
[agent] slow 耗时 121.9 ms ← 生效
第 5 次调用 slow(5) → done-5
[agent] slow 耗时 120.2 ms
第 6 次调用 slow(6) → done-6
…★ 排障留档(本节的另一半价值) ✓✓:
先试默认 REBASE 策略 → [agent] retransform 失败:
java.lang.UnsupportedOperationException: class redefinition failed: attempted to add a method
改为「只改方法体」的手写 transformer → [agent] 已改写 … 1679 → 2072 字节 ✓✓javap -c -p 逐个方法对照源码,确认「你写的」与「编译出的」差异 ✓✓✓;@Advice.Origin("#m #d") 打印方法描述符:同一个 agent 也能服务其他方法 ✓✓;-javaagent 去掉再跑 AgentTarget:确认「没有 agent 就没有耗时日志」✓✓;sc -d(《Arthas 与线上问题诊断》):看类的加载器没变、但字节码变了 ✓✓。| # | 错误 | 说明 |
|---|---|---|
| 1 | manifest 忘写 Premain-Class/Agent-Class ✗✗ | 挂载时静默不生效或报错 ✓✓ |
| 2 | attach 后不 retransform 已加载的类 ✗✗✗ | 主类可能早已加载(本节坑 1)✓✓ |
| 3 | 对已加载类用「新增方法」的增强 ✗✗✗ | attempted to add a method(本节坑 2)✓✓ |
| 4 | agent 依赖没进 classpath / 没打进 jar ✗✗ | agent 类解析失败,挂载后毫无动静 ✓ |
| 5 | 在 advice 里写重逻辑 / 抛异常 ✗✗ | 会污染目标方法执行,甚至拖垮业务 ✓✓ |
| 6 | 以为「增强过就永久生效」 ✗ | 类卸载/重新加载会回到原始字节码 ✓ |
javap 四段字节码对照 ✓✓✓;attempted to add a method ✗✗✓(亲手复现坑比读十遍强);-Djdk.instrument.traceUsage 启动,观察 JDK 对 agent 使用的追踪输出 ✓。验收方式:能独立写出「attach 到指定 pid 并给指定方法加耗时日志」的 agent,并解释每一步为什么必须存在。
扩展任务:给项目 A 写一个「慢方法告警 agent」:耗时超过阈值的调用打印一条带线程名与参数的告警 ✓✓
javap -c 的输出里,iload_1、iadd、ireturn 分别在做什么?invokedynamic + makeConcatWithConstants ✓★premain(-javaagent 静态)/ agentmain(attach 动态) ✓✓★JVM 模块到此收官(《JVM 内存结构与对象创建》 内存结构 → 《类加载机制》 类加载 → 《垃圾回收算法与收集器》 GC → 《GC 日志分析与 JVM 调优》 日志与调优 → 《Arthas 与线上问题诊断》 在线诊断 → 本节)。 下一站:《CAP 与 BASE 理论》 起的分布式与架构设计(CAP 与 BASE、分布式事务、高并发与高可用系统设计)。
暂无