Java Agent的运行方式
- JVM并不会限制Java Agent的数量
- 可以在JVM参数中包含多个-javaagent参数
- 也可以远程attach多个Java Agent
- JVM会按照参数的顺序或者attach的顺序,逐个执行Java Agent
- JRebal/Btrace/arthas等工具都是基于Java Agent实现的
premain
以JVM参数(-javaagent)的方式启动,在Java程序的main方法执行之前执行
MyAgent
1 2 3 4 5 6 7 8
| package me.zhongmingmao;
public class MyAgent { public static void premain(String args) { System.out.println("premain"); } }
|
manifest.txt
1 2 3 4 5 6 7 8 9 10
| # 写入两行数据,最后一行为空行 $ echo 'Premain-Class: me.zhongmingmao.MyAgent ' > manifest.txt
$ tree . ├── manifest.txt └── me └── zhongmingmao └── MyAgent.java
|
编译打包
1 2 3 4 5 6 7 8
| $ javac me/zhongmingmao/MyAgent.java
$ jar cvmf manifest.txt myagent.jar me/ 已添加清单 正在添加: me/(输入 = 0) (输出 = 0)(存储了 0%) 正在添加: me/zhongmingmao/(输入 = 0) (输出 = 0)(存储了 0%) 正在添加: me/zhongmingmao/MyAgent.class(输入 = 399) (输出 = 285)(压缩了 28%) 正在添加: me/zhongmingmao/MyAgent.java(输入 = 142) (输出 = 114)(压缩了 19%)
|
HelloWorld
1 2 3 4 5 6 7 8 9 10
| package helloworld;
import java.util.concurrent.TimeUnit;
public class HelloWorld { public static void main(String[] args) throws InterruptedException { System.out.println("Hello World"); TimeUnit.MINUTES.sleep(1); } }
|
编译运行
1 2 3 4 5
| $ javac helloworld/HelloWorld.java
$ java -javaagent:myagent.jar helloworld.HelloWorld premain Hello World
|
agentmain
- 以Attach的方式启动,在Java程序启动后运行,利用VirtualMachine的Attach API
- Attach API其实是Java进程之间的沟通桥梁,底层通过Socket进行通信
- jps/jmap/jinfo/jstack/jcmd均依赖于Attach API
MyAgent
1 2 3 4 5 6 7
| package me.zhongmingmao;
public class MyAgent { public static void agentmain(String args) { System.out.println("agentmain"); } }
|
manifest.txt
1 2 3 4 5 6 7 8 9 10
| # 改为Agent-Class $ echo 'Agent-Class: me.zhongmingmao.MyAgent ' > manifest.txt
$ tree . ├── manifest.txt └── me └── zhongmingmao └── MyAgent.java
|
编译打包
1 2 3 4 5 6 7 8
| $ javac me/zhongmingmao/MyAgent.java
$ jar cvmf manifest.txt myagent.jar me/ 已添加清单 正在添加: me/(输入 = 0) (输出 = 0)(存储了 0%) 正在添加: me/zhongmingmao/(输入 = 0) (输出 = 0)(存储了 0%) 正在添加: me/zhongmingmao/MyAgent.class(输入 = 401) (输出 = 285)(压缩了 28%) 正在添加: me/zhongmingmao/MyAgent.java(输入 = 146) (输出 = 115)(压缩了 21%)
|
AttachTest
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| import com.sun.tools.attach.VirtualMachine;
public class AttachTest { public static void main(String[] args) throws Exception { if (args.length <= 1) { System.out.println("Usage: java AttachTest <PID> /PATH/TO/AGENT.jar"); return; } String pid = args[0]; String agent = args[1]; VirtualMachine vm = VirtualMachine.attach(pid); vm.loadAgent(agent); } }
|
编译AttachTest
1 2
| # 指定classpath $ javac -cp ~/.sdkman/candidates/java/current/lib/tools.jar AttachTest.java
|
运行HelloWorld
1 2 3 4 5
| $ java helloworld.HelloWorld
$ jps 23386 HelloWorld 23387 Jps
|
运行AttachTest
1
| $ java -cp ~/.sdkman/candidates/java/current/lib/tools.jar:. AttachTest 23386 PATH_TO_AGENT/myagent.jar
|
1 2 3
| # HelloWorld进程继续输出agentmain Hello World agentmain
|
Java Agent的功能
- ClassFileTransformer用于拦截类加载事件,需要注册到Instrumentation
- Instrumentation.redefineClasses
- 针对已加载的类,舍弃原本的字节码,替换为由用户提供的byte数组
- 功能比较危险,一般用于修复出错的字节码
- Instrumentation.retransformClasses
- 针对已加载的类,重新调用所有已注册的ClassFileTransformer的transform方法,两个场景
- 在执行premain和agentmain方法前,JVM已经加载了不少类
- 而这些类的加载事件并没有被拦截并执行相关的注入逻辑
- 定义了多个Java Agent,多个注入的情况,可能需要移除其中的部分注入
- 调用Instrumentation.removeTransformer去除某个注入类后,可以调用retransformClasses
- 重新从原始byte数组开始进行注入
- Java Agent的功能是通过JVMTI Agent(C Agent),JVMTI是一个事件驱动的工具实现接口
- 通常会在C Agent加载后的方法入口Agent_OnLoad处注册各种事件的钩子方法
- 当JVM触发这些事件时,便会调用对应的钩子方法
- 例如可以为JVMTI中的ClassFileLoadHook事件设置钩子,从而在C层面拦截所有的类加载事件
获取魔数
MyAgent
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| package me.zhongmingmao;
import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.IllegalClassFormatException; import java.lang.instrument.Instrumentation; import java.security.ProtectionDomain;
public class MyAgent { public static void premain(String args, Instrumentation instrumentation) { instrumentation.addTransformer(new MyTransformer()); }
static class MyTransformer implements ClassFileTransformer { @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { System.out.printf("Loaded %s: 0x%X%X%X%X\n", className, classfileBuffer[0], classfileBuffer[1], classfileBuffer[2], classfileBuffer[3]); return null; } } }
|
编译运行
1 2 3 4 5 6 7
| $ java -javaagent:myagent.jar helloworld.HelloWorld ... Loaded helloworld/HelloWorld: 0xCAFEBABE Hello World ... Loaded java/lang/Shutdown: 0xCAFEBABE Loaded java/lang/Shutdown$Lock: 0xCAFEBABE
|
ASM注入字节码
通过ASM注入字节码可参考Instrumenting Java Bytecode with ASM
MyAgent
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| package me.zhongmingmao;
import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.Opcodes; import org.objectweb.asm.tree.*;
import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.IllegalClassFormatException; import java.lang.instrument.Instrumentation; import java.security.ProtectionDomain;
public class MyAgent { public static void premain(String args, Instrumentation instrumentation) { instrumentation.addTransformer(new MyTransformer()); }
static class MyTransformer implements ClassFileTransformer, Opcodes { @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { ClassReader classReader = new ClassReader(classfileBuffer); ClassNode classNode = new ClassNode(ASM7); classReader.accept(classNode, ClassReader.SKIP_FRAMES);
for (MethodNode methodNode : classNode.methods) { if ("main".equals(methodNode.name)) { InsnList instrumentation = new InsnList(); instrumentation.add(new FieldInsnNode(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;")); instrumentation.add(new LdcInsnNode("Hello, Instrumentation!")); instrumentation.add(new MethodInsnNode(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false)); methodNode.instructions.insert(instrumentation); } }
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS); classNode.accept(classWriter); return classWriter.toByteArray(); } } }
|
编译MyAgent
1
| $ javac -cp PATH_TO_ASM/asm-7.0.jar:PATH_TO_ASM_TREE/asm-tree-7.0.jar me/zhongmingmao/MyAgent.java
|
运行
1 2 3
| $ java -javaagent:myagent.jar -cp PATH_TO_ASM/asm-7.0.jar:PATH_TO_ASM_TREE/asm-tree-7.0.jar:. helloworld.HelloWorld Hello, Instrumentation! Hello World
|
基于字节码注入的profiler
统计新建实例数量
MyProfiler
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| package me.zhongmingmao;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger;
public class MyProfiler { public static ConcurrentHashMap<Class<?>, AtomicInteger> data = new ConcurrentHashMap<>();
public static void fireAllocationEvent(Class<?> klass) { data.computeIfAbsent(klass, kls -> new AtomicInteger()).incrementAndGet(); }
public static void dump() { data.forEach((kls, counter) -> System.err.printf("%s: %d\n", kls.getName(), counter.get())); }
static { Runtime.getRuntime().addShutdownHook(new Thread(MyProfiler::dump)); } }
|
MyAgent
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| package me.zhongmingmao;
import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; import org.objectweb.asm.tree.*;
import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.IllegalClassFormatException; import java.lang.instrument.Instrumentation; import java.security.ProtectionDomain;
public class MyAgent { public static void premain(String args, Instrumentation instrumentation) { instrumentation.addTransformer(new MyTransformer()); }
static class MyTransformer implements ClassFileTransformer, Opcodes { @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { if (className.startsWith("java") || className.startsWith("javax") || className.startsWith("jdk") || className.startsWith("sun") || className.startsWith("com/sun") || className.startsWith("me/zhongmingmao")) { return null; }
ClassReader classReader = new ClassReader(classfileBuffer); ClassNode classNode = new ClassNode(ASM7); classReader.accept(classNode, ClassReader.SKIP_FRAMES);
for (MethodNode methodNode : classNode.methods) { for (AbstractInsnNode node : methodNode.instructions.toArray()) { if (node.getOpcode() == NEW) { TypeInsnNode typeInsnNode = (TypeInsnNode) node; InsnList instrumentation = new InsnList(); instrumentation.add(new LdcInsnNode(Type.getObjectType(typeInsnNode.desc))); instrumentation.add(new MethodInsnNode(INVOKESTATIC, "me/zhongmingmao/MyProfiler", "fireAllocationEvent", "(Ljava/lang/Class;)V", false)); methodNode.instructions.insert(node, instrumentation); } } }
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS); classNode.accept(classWriter); return classWriter.toByteArray(); } } }
|
ProfilerMain
1 2 3 4 5 6 7 8 9 10 11 12
| public class ProfilerMain { public static void main(String[] args) { String s = ""; for (int i = 0; i < 10; i++) { s = new String("" + i); } Integer i = 0; for (int j = 0; j < 20; j++) { i = new Integer(j); } } }
|
运行
1 2 3 4
| $ java -javaagent:myagent.jar -cp PATH_TO_ASM/asm-7.0.jar:PATH_TO_ASM_TREE/asm-tree-7.0.jar:. ProfilerMain java.lang.StringBuilder: 10 java.lang.String: 10 java.lang.Integer: 20
|
命名空间
- 不少应用程序都依赖于ASM工程,当注入逻辑依赖于ASM时
- 可能会出现注入使用最新版的ASM,而应用程序本身使用的是较低版本的ASM
- JDK本身也使用了ASM库,例如用来生成Lambda表达式的适配器,JDK的做法是重命名整个ASM库
- 还有另外一个方法是借助自定义类加载器来隔离命名空间
观察者效应
- 例如字节码注入收集每个方法的运行时间
- 假设某个方法调用了另一个方法,而这个两个方法都被注入了
- 那么统计被调用者运行时间点注入代码所耗费的时间,将不可避免地被计入至调用者方法的运行时间之中
- 统计新建对象数量
- 即时编译器的逃逸分析可能会优化掉新建对象操作,但它并不会消除相关的统计操作
- 因此会统计到实际没有发生的新建对象操作
- 因此当使用字节码注入开发profiler,仅能表示在被注入的情况下程序的执行状态
参考资料
深入拆解Java虚拟机