以 JVM参数 (-javaagent)的方式启动,在Java程序的main方法执行之前执行
package me.zhongmingmao; public class MyAgent { // JVM能识别的premain方法接收的是字符串类型的参数,并非类似main方法的字符串数组 public static void premain(String args) { System.out.println("premain"); } }
# 写入两行数据,最后一行为空行 $ echo 'Premain-Class: me.zhongmingmao.MyAgent ' > manifest.txt $ tree . ├── manifest.txt └── me └── zhongmingmao └── MyAgent.java
$ 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%)
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); } }
$ javac helloworld/HelloWorld.java $ java -javaagent:myagent.jar helloworld.HelloWorld premain Hello World
package me.zhongmingmao; public class MyAgent { public static void agentmain(String args) { System.out.println("agentmain"); } }
# 改为Agent-Class $ echo 'Agent-Class: me.zhongmingmao.MyAgent ' > manifest.txt $ tree . ├── manifest.txt └── me └── zhongmingmao └── MyAgent.java
$ 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%)
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]; // Attach API VirtualMachine vm = VirtualMachine.attach(pid); vm.loadAgent(agent); } }
编译AttachTest
# 指定classpath $ javac -cp ~/.sdkman/candidates/java/current/lib/tools.jar AttachTest.java
$ java helloworld.HelloWorld $ jps 23386 HelloWorld 23387 Jps
$ java -cp ~/.sdkman/candidates/java/current/lib/tools.jar:. AttachTest 23386 PATH_TO_AGENT/myagent.jar
# HelloWorld进程继续输出agentmain Hello World agentmain
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来注册类加载事件的拦截器(实现ClassFileTransformer.transform) 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 { // 返回的byte数组,代表更新后的字节码 // 当transform方法返回时,JVM会使用返回的byte数组来完成接下来的类加载工作 // 如果transform方法返回null或者抛出异常,JVM将使用原来的byte数组来完成类加载工作 // 基于类加载事件的拦截功能,可以实现字节码注入(Bytecode instrumentation),往正在被加载的类插入额外的字节码 System.out.printf("Loaded %s: 0x%X%X%X%X/n", className, classfileBuffer[0], classfileBuffer[1], classfileBuffer[2], classfileBuffer[3]); return null; } } }
$ java -javaagent:myagent.jar helloworld.HelloWorld ... Loaded helloworld/HelloWorld: 0xCAFEBABE Hello World ... Loaded java/lang/Shutdown: 0xCAFEBABE Loaded java/lang/Shutdown$Lock: 0xCAFEBABE
通过ASM注入字节码可参考 Instrumenting Java Bytecode with ASM
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 { // 将classfileBuffer转换为ClassNode ClassReader classReader = new ClassReader(classfileBuffer); ClassNode classNode = new ClassNode(ASM7); classReader.accept(classNode, ClassReader.SKIP_FRAMES); // 遍历ClassNode的MethodNode节点,即构造器和方法 for (MethodNode methodNode : classNode.methods) { // 在main方法入口处注入System.out.println("Hello Instrumentation"); 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
$ javac -cp PATH_TO_ASM/asm-7.0.jar:PATH_TO_ASM_TREE/asm-tree-7.0.jar me/zhongmingmao/MyAgent.java
$ 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
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)); } }
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")) { // Skip JDK classes and profiler classes // 避免循环引用,从而导致StackOverflowException 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) { // 在每条new指令后插入对fireAllocationEvent方法的调用 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(); } } }
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); } } }
$ 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