在前面的介绍中,我们看到JVM TI的强大功能,然而,对于不熟悉C/C++语言的Java程序员来说,这扇门是不是真的就完全关闭了呢?还好,在关上了门的同时,JVM为我们提供了另一扇窗——Java Instrutment。其实Java Instrutment只提供了JVM TI中非常小的一个功能子集,一个是允许在类加载之前,修改类字节(ClassFileTransformer)(JDK5中开始提供,即使随JVM启动的Agent),另外一个是在类加载之后,触发JVM重新进行类加载(JDK6中开始提供,用于JVM启动之后通过Attach去加载Agent)。这两个功能表面看起来微不足道,但实际非常强大,AspectJ AOP的动态Weaving、Visual VM的性能剖析、JConsole支持Attach到进程上进行监控,都是通过这种方式来做的。除了这两个功能外,JDK 6中还提供了动态增加BootstrapClassLoader/SystemClassLoader的搜索路径、对Native方法进行instrutment(还记得JVM TI的Native Method Bind吗?)。
1.主要API(java.lang.instrutment)
1)ClassFileTransformer:定义了类加载前的预处理类,可以在这个类中对要加载的类的字节码做一些处理,譬如进行字节码增强
2)Instrutmentation:增强器,由JVM在入口参数中传递给我们,提供了如下的功能
我们后面通过范例来了解如何使用Java Instrutment技术。
2.随JVM启动的Agent
1)定义入口
public class StubPreMain { //另外一种入口格式是public static void premain(String agentArgs) public static void premain(String agentArgs, Instrumentation inst) throws ClassNotFoundException, UnmodifiableClassException { inst.addTransformer(new StubTransformer()); System.out.println("StubPreMain:Add StubTransformer"); } }
我们的入口类格式必须是如上的premain(对应JVM TI的Agent_OnLoad方法),与JVM TI类似,JVM启动的时候回回调这个入口函数。在premain中我们最常见就是增加Transform,Transform允许我们在类加载器修改bytecode。一般性能剖析程序都是通过修改字节码,在方法进入和退出时收集时间数据来得出剖析数据的。
2)编写Transform
public class StubTransformer implements ClassFileTransformer { /* * (non-Javadoc) * * @see java.lang.instrument.ClassFileTransformer#transform(java.lang.ClassLoader, * java.lang.String, java.lang.Class, java.security.ProtectionDomain, * byte[]) */ public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { if ("ray.WebStub".equals(className)) { System.out.println("Load WebStub From Transformer"); return readByte("WebStub.classbyte"); } return null; } public byte[] readByte(String fileName) { ByteArrayOutputStream baos = new ByteArrayOutputStream(); InputStream is = StubTransformer.class .getResourceAsStream(fileName); int BUFFER_SIZE = 1024; byte[] buffer = new byte[BUFFER_SIZE]; int length = BUFFER_SIZE; while (length == BUFFER_SIZE) { try { length = is.read(buffer); baos.write(buffer, 0, length); } catch (IOException e) { e.printStackTrace(); } } return baos.toByteArray(); } }
这里简单地使用一个编译好的新的class的字节替换掉旧的class的字节,实际中我们当然不会使用这么苯的方法,实际上有各种字节码操作工具可以辅助我们完成这个任务
3)修改META-INF/ MANIFEST.MF
4)打成stubagent.jar,在启动参数中增加如下启动参数,启动JVM,当加载ray.WebStub这个类的时候,字节码会变成我们传进去的新的字节码
3.通过Attach在JVM运行期间加载的Agent
这种Agent是JDK6才开始支持的,因此使用一般还是比较少的,我们来看一个实际使用范例,在前面的JVM Management API 的介绍中,我们知道,通过Attach API连接到JVM上,然后加载management-agent.jar,就可以在JVM中启动一个JMX代理。实际上management-agent.jar是一个支持通过Attach在JVM运行期间加载的Agent。management-agent.jar实际上只有一个META-INF/ MANIFEST.MF文件,内容如下:
可以看出这个包实际上既支持随JVM一起启动(我们前面看到的Premain-Class配置),也支持在JVM启动后通过Attach API去加载启动(看Agent-Class配置),我们通过反汇编看看sun.management.Agent是怎么做的。
sun.management.Agent定义了入口函数agentmain(对应于Agent_OnAttach方法)
public class Agent { public static void premain(String agentArgs) throws Exception { agentmain(s); } public static void agentmain(String agentArgs) throws Exception { …. } }
与Premian-Agetn类似,另外一种入口函数格式是public static void premain(String agentArgs, Instrumentation inst)。