之前在看agentzh的此篇博文动态追踪技术漫谈时,领会到了动态追踪技术的强大之处,也一直由于无法在不重启线上服务器的情况下排查线上问题在寻找Java中的动态追踪工具。在公司内部的JavaEE性能检测框架中,我们使用了asm做字节码注入来做线上性能的监测,沿着这个思路,如果要做到动态追踪应该是需要做字节码注入的,但是额外的一点是需要动态加载字节码替换掉原有的类的。此外,性能监测框架是需要耦合到业务应用中的,无法做到一个监测工具的灵活性。

后来听同事提到了BTrace这个工具,于是去尝试了一下。BTrace是SUN Kenai云计算开发平台下的一个开源项目,旨在为java提供安全可靠的动态跟踪分析工具。江南白衣的这篇文章http://calvin1978.blogcn.com/articles/btrace1.html做了比较详细的描述。

那么,BTrace这么神奇的功能是如何实现的呢?既然这是个开源的代码,那么直接从代码找原理。BTrace代码开源在https://github.com/btraceio/btrace

总体来说,BTrace是基于动态字节码修改技术(Hotswap)来实现运行时java程序的跟踪和替换。大体的原理可以用下面的公式描述:

Client(Java compile api + attach api) + Agent(脚本解析引擎 + ASM + JDK6 Instumentation) + Socket

BTrace的入口类在https://github.com/btraceio/btrace/blob/master/src/share/classes/com/sun/btrace/client/Main.java中。在其main方法中,可以看到起最终的核心逻辑是在https://github.com/btraceio/btrace/blob/master/src/share/classes/com/sun/btrace/client/Client.java中。方法调用如下:

  • client.compile
  • client.attach
  • client.submit

Client

首先是client.compile方法,使用的是Java compile api,将我们传递的java源文件编译为.class文件,当然你如果使用btracec提前编译了源代码,那么这里就不会有这一步。

针对官方脚本的一个例子:

import com.sun.btrace.annotations.*;
import static com.sun.btrace.BTraceUtils.*;
@BTrace
public class HelloWorld {
    @OnMethod(
        clazz="java.lang.Thread",
        method="start"
    )
    public static void func() {
        println("about to start a thread!");
    }
}

@OnMethod告诉Btrace解析引擎需要代理的类和方法。 这个例子的作用是当java.lang.Thread类的任意一个对象调用 start 方法后,会调用func方法。

client端在编译完脚本之后,进行了一次字节码修改,但是仅仅是做了一些兼容性,例如域访问控制器、简写等。

接着client.attach中使用java的attach api将agent动态attach到目标jvm进程中(ava agent,通常有两种方式添加到jvm进程中:动态attach;在目标jvm启动之前添加agent参数)。

VirtualMachine vm  = VirtualMachine.attach(pid);
...
vm.loadAgent(agentPath, agentArgs);

最后client的submit方法,会向agent发送监控命令以及传递对应code的字节码。

Agent

BTrace的agent实现类就在https://github.com/btraceio/btrace/blob/master/src/share/classes/com/sun/btrace/agent/Main.java中,具体的实现可以看其main方法,此agent的premain和agentmain方法都是调用了这个方法。这里需要注意的一点:必须要上jdk6,因为jdk5虽然已经有了instrument api,但是其仅仅支持premain方法,也就是仅仅支持在main方法运行之前执行一些动作,而jdk6后加入了agentmain方法和VirtualMachine,是可以在main方法运行后执行的(如果是通过命令行启动的,那么agentmain方法不会被调用)。此外,在jdk6之前,程序启动之后是无法再设置boot class加载路径和system class加载路径的。而jdk6之后,instrument新增的appendToBootstrapClassLoaderSearch和appendToSystemClassLoaderSearch是可以动态添加classpath的。

agent被提交到目标jvm进程后,首先会添加boot classpath.

...
inst.appendToBootstrapClassLoaderSearch(jf);
...
inst.appendToSystemClassLoaderSearch(jf);

接着开启一个serversocket等待client的连接。之后client和agent之间的数据通讯,比如生成.class发送到agent,agent将线上程序打印的数据回传给 client都是通过socket来进行的。当agent接收到监控命令后,主要有以下两部分工作:

  • 重写类:遍历当前所有的class,根据正则找到匹配的类,用asm重写
  • 替换类:替换掉原来的class

agent接受到client发来的监控指令以及对应的参数后,会load所有的class,根据正则去匹配指定的类和方法,并使用脚本解析引擎去处理发送过来的字节码然后使用ASM将脚本里标注的类java.lang.Thread的字节码重写,植入跟踪代码或新的逻辑。在上面那个例子中,Java.lang.Thread这个类的字节码被重写并在start方法体尾部植入了func方法的调用。

BTrace的agent利用instrumentation的retransformClasses方法将原始字节码替换掉,使用的transfomer见https://github.com/btraceio/btrace/blob/master/src/share/classes/com/sun/btrace/runtime/BTraceTransformer.java。如下:

new ClassFileTransformer() {
    public byte[] transform(ClassLoader l, String className, Class c, ProtectionDomain pd, byte[] b) throws IllegalClassFormatException {
        // BTrace解析脚本,利用asm重写bytecode,然后classLoader加载
    }
}, true);

其中,在agent的agentmain中通过handleNewClient方法启动一个异步线程进行class transformer,而在这个异步线程中最终是通过调用https://github.com/btraceio/btrace/blob/master/src/share/classes/com/sun/btrace/agent/Client.java中的retransformLoaded()来进行的。

总结

其实BTrace就是使用了java attach api附加agent.jar,然后使用脚本解析引擎+asm来重写指定类的字节码,再使用instrument实现对原有类的替换。借鉴这些,我们也完全可以实现自己的动态追踪工具。