今天,Team Leader推荐了一个非常棒的动态跟踪分析工具 – BTrace。由于对它的实现原理非常感兴趣,于是花了点时间研究了一下,顺便写点心得。
什么是BTrace?
BTrace是SUN Kenai云计算开发平台下的一个开源项目。旨在为java提供安全可靠的动态跟踪分析工具。
Btrace基于动态字节码修改技术(Hotswap)来实现运行时java程序的跟踪和替换。(还记得javarebel不?)
Btrace的脚本是用纯java编写的,基于一套官方提供的annotation,使跟踪逻辑实现起来异常简单。
实现原理
用一个简单的公式来表述(从左往右的使用顺序):
Sun Attach API + BTrace脚本解析引擎 + Objectweb ASM + JDK6 Instumentation
1,Sun Attach API是充当动态加载 agent 的角色。
看下面的例子:
VirtualMachine vm = VirtualMachine.attach("1688"); // 1688是进程id
String agentJarPath = "E:\agent.jar"; // agent jar路径
vm.loadAgent(agentJarPath); // 加载agent
这个例子动态地为一个已经启动的java附加一个agent上去.
2,BTrace解析引擎解析BTrace脚本。
摘自官方的一个例子:
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 方法。
3,解析完脚本后,Btrace会使用ASM将脚本里标注的类java.lang.Thread的字节码重写,植入跟踪代码或新的逻辑。
在上面那个例子中,Java.lang.Thread 这个类的字节码被重写了。并在start方法体尾部植入了 func 方法的调用.
ASM的使用,本文不做延伸。
4,利用instrumentation的retransformClasses,将原始字节码替换掉。
instrumentation例子:
import java.lang.instrument.ClassDefinition;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
public class AgentMain {
public static void agentmain(String agentArgs, Instrumentation inst)
throws ClassNotFoundException, UnmodifiableClassException,
InterruptedException {
inst.addTransformer(new ClassFileTransformer() {
public byte[] transform(ClassLoader l, String className, Class<?> c, ProtectionDomain pd, byte[] b) throws IllegalClassFormatException {
// BTrace解析脚本,利用asm重写bytecode,然后classLoader加载
}
}, true);
inst.retransformClasses(java.lang.Thread.class);
}
}
关于instrumentation的一些猜测
因为 instrumentation 不能添加,修改方法,字段名,所以我怀疑它的实现原理是用 JIT 生成机器码,利用机器码的优先执行来做class替换的。JIT本身会将调用次数频繁的方法编译成机器码。
还有一种猜测就是,备份堆里的老bytecode,然后直接替换为新的。不过这种方式貌似有点暴力。
另外,替换后的字节码是在新线程内才会生效的,老线程依旧用老的字节码在执行。
替换的类原有的字段值是保持不变的。
局限性
can not create new objects.
can not create new arrays.
can not throw exceptions.
can not catch exceptions.
can not make arbitrary instance or static method calls – only the public static methods of com.sun.btrace.BTraceUtils class may be called from a BTrace program.
can not assign to static or instance fields of target program’s classes and objects. But, BTrace class can assign to it’s own static fields (“trace state” can be mutated).
can not have instance fields and methods. Only static public void returning methods are allowed for a BTrace class. And all fields have to be static.
can not have outer, inner, nested or local classes.
can not have synchronized blocks or synchronized methods.
can not have loops (for, while, do..while)
can not extend arbitrary class (super class has to be java.lang.Object)
can not implement interfaces.
can not contains assert statements.
can not use class literals.
上面是摘自官方的使用限制列表,从内容上可以看,BTrace的神通仅仅局限于只读操作。不仅强制要求java脚本需要提供public static方法.
而且,脚本里无法实例化对象,数组,不能抛异常或捕捉,不能有循环,内部类等等。
针对一些特殊对象,BTrace也是无能为力的。比如java.lang.Integer,Array等。
不过话说回来,BTrace应付大部分应用场景还是绰绰有余的。
打破局限性约束
1,自己做instrumentation的类替换,绕过BTrace的安全检查。
2,基于JVM TI自己写工具,上面的局限性将荡然无存,并且可以实现的功能会多很多。
参考资料:
BTrace 用户指南 http://kenai.com/projects/btrace/pages/UserGuide
BTrace 开发者指南 http://kenai.com/projects/btrace/pages/DeveloperGuide
Instrumentation文档http://java.sun.com/javase/6/docs/technotes/guides/instrumentation/index.html
Attach API 文档 http://java.sun.com/javase/6/docs/technotes/guides/attach/index.html