写在前面的话:Btrace系列是我将平时里学习和使用Btrace的一些经验的总结,拿出来和大家一起交流一下,希望在这个过程中能找寻出自己理解或使用上的错误之处。
一、Btrace的简介:
Btrace是由Kenai 开发的一个开源项目,是一种动态跟踪分析JAVA源代码的工具。它可以用来帮我们做运行时的JAVA程序分析,监控等等操作,当然,它也不是万能的,BTrace也有一些使用上的限制,如:不能在脚本中新建类等等,这些在官方网站上有很详细的介绍,大家有兴趣可以查看:http://kenai.com/projects/btrace/pages/UserGuide。
二、JDK6的几个新特性:
Btrace是由:Attach API + BTrace脚本解析引擎 + ASM + JDK6 Instumentation组成,这里要注意最后一项是JDK6的Instumentation,为什么一定要是JDK6呢?我们就要来看一下JDK6为我们提供了什么:
1、虚拟机启动后的Instumentation:
Instumentation早在JDK5的时候就已经提出了,但是它有个局限性,premain函数只能在main函数之前被运行(对于Instumentation不熟悉的请去Sun官网查看),而JDK6之后提供了一个叫做agentmain的函数,它可以在main函数运行后在运行,代码如下:
public static void agentmain(String args, Instrumentation inst) public static void agentmain(String args)
这个函数的功能通premain函数一样,可以对类进行各种操作。同premain函数一样,在manifest 文件里面设置“Agent-Class”来指定包含 agentmain 函数的类。
2、Instumentation提供的新方法retransformClasses:
这个新方法可以在agentmain函数中调用,它的功能和redefineClasses 一样,可以修改类的定义且是批量的。
3、BootClassPath和SystemClassPath的动态指定:
在JDK6之前,我们知道可以通过系统参数或者虚拟机启动参数,设置一个虚拟机运行时的boot class加载路径和system class加载路径,但是在启动后,这个路径是不可以修改的,并且,我们要在启动后再去加载一个*.jar文件是不可能的,但是在JDK6以后,这个规定被打破了,可以使用Instrumentation的appendToBootstrapClassLoaderSearch和appendToSystemClassLoaderSearch来修改路径或加载新的*.jar(注意:虽然实际的classpath被修改了,但是在property中的java.class.path却没有受任何影响)。
正因为以上的新特性,才缔造出了Btrace的想法以至于最后的实现。
三、Btrace的原理:
Btrace首先是通过Attach API中提供的VirtualMachine.attach(PID)方法来获得要监控的JVM,然后使用VirtualMachine.loadAgent("*.jar")方法来加载jar文件,这个jar文件中会包含Btrace的一个很重要的类com.sun.btrace.agent.Main,这个类里定义了如下的函数:
public static void premain(String args, Instrumentation inst) { main(args, inst); } public static void agentmain(String args, Instrumentation inst) { main(args, inst); }
这里两个函数都调用了一个main方法,如下:
private static synchronized void main(final String args, final Instrumentation inst) { ...... inst.appendToBootstrapClassLoaderSearch(new JarFile(new File(path))); ...... inst.appendToSystemClassLoaderSearch(new JarFile(new File(path))); ...... startServer(); }
这里省去了不必要的代码,主要三行代码,头两行不用解释,上面已经说过用途了,第三行解释一下,代码如下:
private static void startServer() { ...... while (true) { try { ...... handleNewClient(client); } catch (RuntimeException re) { if (isDebug()) debugPrint(re); } catch (IOException ioexp) { if (isDebug()) debugPrint(ioexp); } } }
关键看一下handleNewClient(client)方法的调用,这个是修改类定义的地方,如下:
private static void handleNewClient(final Client client) { ...... inst.addTransformer(client, true); ...... inst.retransformClasses(classes); }
这两句话就实现了对现有内存中的类定义的替换,当在一次调用new创建一个新对象时就会使用新的类定义,而老的已经生成的类对象是不会收到干扰的。
那又是谁取执行了对类定义的修改呢,这个是由BTrace脚本解析引擎 + ASM来实现的,脚本引擎负责解析我们所写的脚本,而ASM来对JAVA的字节码进行增强修改。你在Btrace的com.sun.btrace.agent.Client中可以看到ASM的影子,代码如下:
abstract class Client implements ClassFileTransformer, CommandListener { static { ClassFilter.class.getClass(); ClassReader.class.getClass(); ClassWriter.class.getClass(); ...... } private byte[] instrument(Class clazz, String cname, byte[] target) { byte[] instrumentedCode; try { ClassWriter writer = InstrumentUtils.newClassWriter(target); ClassReader reader = new ClassReader(target); Instrumentor i = new Instrumentor(clazz, className, btraceCode, onMethods, writer); ...... }
现在我们在回顾一下整个流程,用Attach API附加*.jar然后使用BTrace脚本解析引擎 + ASM来实现对类的修改,在使用Instumentation实现类的内存替换,完毕!perfect!BTrace原来也就这么回事,但是我们不得不佩服开发团队的思维和整合能力,向牛人致敬!