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虚拟机