来源:美团技术博客
目录
Java对象行为
java.lang.instrument.Instrumentation
直接操作字节码
BTrace
Arthas
三生万物
在遥远的希艾斯星球爪哇国塞沃城中,两名年轻的程序员正在为一件事情苦恼,程序出问题了,一时看不出问题出在哪里,于是有了以下对话:
“我还没到达只通过Review代码就能发现问题的至高境界。”
Java对象行为
public class Person{ private int age; private String name; public void speak(String str) { System.out.println(str); } public Person(int age, String name) { this.age = age; this.name = name; }}上面Person类中age和name是属性,speak是行为。对象是类的实例,每个对象的属性都属于对象本身,但是每个对象的行为却是公共的。举个例子,比如我们现在基于Person类创建了两个对象,personA和personB:
Person personA = new Person(43, "lixunhuan");personA.speak("我是李寻欢");Person personB = new Person(23, "afei");personB.speak("我是阿飞");personA和personB有各自的姓名和年龄,但是有共同的行为:speak。想象一下,如果我们是Java语言的设计者,我们会怎么存储对象的行为和属性呢?
“很简单,属性跟着对象走,每个对象都存一份。行为是公共的东西,抽离出来,单独放到一个地方。”
“咦?抽离出公共的部分,跟代码复用好像啊。”
“大道至简,很多东西本来都是殊途同归。”
也就是说,第一步我们首先得找到存储对象行为的这个公共的地方。一番搜索之后,我们发现这样一段描述:
Method area is created on virtual machine startup, shared among all Java virtual machine threads and it is logically part of heap area. It stores per-class structures such as the run-time constant pool, field and method data, and the code for methods and constructors.
java.lang.instrument.Instrumentation。”java.lang.instrument.Instrumentation
看完文档之后,我们发现这么两个接口:redefineClasses和retransformClasses。一个是重新定义class,一个是修改class。这两个大同小异,看redefineClasses的说明:
This method is used to replace the definition of a class without reference to the existing class file bytes, as one might do when recompiling from source for fix-and-continue debugging. Where the existing class file bytes are to be transformed (for example in bytecode instrumentation) retransformClasses should be used.
都是替换已经存在的class文件,redefineClasses是自己提供字节码文件替换掉已存在的class文件,retransformClasses是在已存在的字节码文件上修改后再替换之。
当然,运行时直接替换类很不安全。比如新的class文件引用了一个不存在的类,或者把某个类的一个field给删除了等等,这些情况都会引发异常。所以如文档中所言,instrument存在诸多的限制:
The redefinition may change method bodies, the constant pool and attributes. The redefinition must not add, remove or rename fields or methods, change the signatures of methods, or change inheritance. These restrictions maybe be lifted in future versions. The class file bytes are not checked, verified and installed until after the transformations have been applied, if the resultant bytes are in error this method will throw an exception.
直接操作字节码
到这里,我们知道了用ASM框架直接操作class文件,在类中加一段打印日志的代码,然后retransform就可以了。
BTrace
- 在我们的工程中,谁来做这个寻找字节码,修改字节码,然后retransform的动作呢?我们并非先知,不可能知道未来有没有可能遇到文章开头的这种问题。考虑到性价比,我们也不可能在每个工程中都开发一段专门做这些修改字节码、重新加载字节码的代码。
- 如果JVM不在本地,在远程呢?
- 如果连ASM都不会用呢?能不能更通用一些,更“傻瓜”一些。
A safe, dynamic tracing tool for the Java platform.
package com.sun.btrace.samples;import com.sun.btrace.annotations.*;import com.sun.btrace.AnyType;import static com.sun.btrace.BTraceUtils.*;/** * This sample demonstrates regular expression * probe matching and getting input arguments * as an array - so that any overload variant * can be traced in "one place". This example * traces any "readXX" method on any class in * java.io package. Probed class, method and arg * array is printed in the action. */@BTrace public class ArgArray { @OnMethod( clazz="/java\\.io\\..*/", method="/read.*/" ) public static void anyRead(@ProbeClassName String pcn, @ProbeMethodName String pmn, AnyType[] args) { println(pcn); println(pmn); printArray(args); }}再来看另一个例子:每隔2秒打印截止到当前创建过的线程数。
package com.sun.btrace.samples;import com.sun.btrace.annotations.*;import static com.sun.btrace.BTraceUtils.*;import com.sun.btrace.annotations.Export;/** * This sample creates a jvmstat counter and * increments it everytime Thread.start() is * called. This thread count may be accessed * from outside the process. The @Export annotated * fields are mapped to jvmstat counters. The counter * name is "btrace." + <className> + "." + <fieldName> */@BTrace public class ThreadCounter { // create a jvmstat counter using @Export @Export private static long count; @OnMethod( clazz="java.lang.Thread", method="start" ) public static void onnewThread(@Self Thread t) { // updating counter is easy. Just assign to // the static field! count++; } @OnTimer(2000) public static void ontimer() { // we can access counter as "count" as well // as from jvmstat counter directly. println(count); // or equivalently ... println(Counters.perfLong("btrace.com.sun.btrace.samples.ThreadCounter.count")); }}BTrace脚本:利用BTrace定义的注解,我们可以很方便地根据需要进行脚本的开发。
Compiler:将BTrace脚本编译成BTrace class文件。
Client:将class文件发送到Agent。
Agent:基于Java的Attach API,Agent可以动态附着到一个运行的JVM上,然后开启一个BTrace Server,接收client发过来的BTrace脚本;解析脚本,然后根据脚本中的规则找到要修改的类;修改字节码后,调用Java Instrument的retransform接口,完成对对象行为的修改并使之生效。
整个BTrace的架构大致如下:

btrace工作流程
- 不允许创建对象
- 不允许创建数组
- 不允许抛异常
- 不允许catch异常
- 不允许随意调用其他对象或者类的方法,只允许调用com.sun.btrace.BTraceUtils中提供的静态方法(一些数据处理和信息输出工具)
- 不允许改变类的属性
- 不允许有成员变量和方法,只允许存在static public void方法
- 不允许有内部类、嵌套类
- 不允许有同步方法和同步块
- 不允许有循环
- 不允许随意继承其他类(当然,java.lang.Object除外)
- 不允许实现接口
- 不允许使用assert
- 不允许使用Class对象