背景
周五下班回家,在公司班车上觉得无聊,看了下btrace的源码(自己反编译)。 一些关于btrace的基本内容,可以看下我早起的一篇记录:btrace记忆
上一篇主要介绍的是btrace的一些基本使用以及api,这里我想从btrace源码本身进行下介绍。至于btrace的优势,能用来干些什么,自己上他的官网看下或者google一下,花个半小时就能明白了。
至于为什么会去反编译查看btrace源码,主要是会在部门整个关于btrace的分享。同时btrace的相关技术文档缺乏,javadoc很多时候说的不明不白,作者也没有提供源码开源,所以就有了这次的分享。
Btrace涉及相关技术
- asm
- instrument http://download.oracle.com/javase/6/docs/api/java/lang/instrument/package-summary.html
- JVM TI(java tool api) http://download.oracle.com/javase/6/docs/jdk/api/attach/spec/com/sun/tools/attach/VirtualMachine.html
- Java Compiler Api http://download.oracle.com/javase/6/docs/api/javax/tools/package-summary.html
大家可以先去预备一下知识。
Btrace的大体设计
下面来看一个Btrace的设计图:
说明:
1. BtraceClient : 为我们使用的btrace的本地api,一般我们使用的bin/btrace会在本地启动一个btrace jvm,其内部使用了Java Complier Api, JVMTI技术,以及创建了一个socket。
- Java Complier Api:动态的将我们传递的监控的java源文件动态编译成.class文件
- JVMTI: 主要是利用了java 1.6之后的VirtaulMachine技术,动态的attach到一个已启动的jvm上,为他去启动一个BtraceAgent。该Agent会为BtraceClient启动一个server socket进行通讯。(多进程之间的通讯)
- 本地socket: BtraceClient和BtraceAgent之间的数据通讯,比如生成的.class发送到BtraceAgent,还有一些Event事件等等。BtraceAgent同样可以将服务端print()的数据通过socket的方式回传给BtraceClient进行打印
2. BtraceAgent:为我们在目标jvm上植入的btrace agent实现。主要是Instrumentation技术, asm字节码处理技术。
- BtraceAgent的启动可以有两种方式: BtaceClient动态attach后进行启动, 另一种就是在目标jvm启动之前添加agent参数进行启动。
- BtraceAgent会启动一个server socket,与BtraceClient客户端进行交互,客户端可以将监控的.class文件通过socket发送,同样也可以在jvm启动时直接指定对应本地的.class做为监控脚本。
- BtraceAgent在接受到监控指令后,会遍历当前所有已经加载的class类,挨个进行匹配检查,并生成相应的监控字节码(监控方法)。
btrace的包结构:
- btrace-client.jar
- btrace-boot.jar
- btrace-agent.jar
btrace-client
一般我们通常直接使用的命令,比如:
- bin/btrace $pid Btrace.java
都是直接调用了btrace-client包中的代码。
几个核心类介绍:
1. com.sun.btrace.client.Main (btrace的启动入口)
- 调用Client进行complier(Java Compiler Api)和attach(JVM TI)处理
2. com.sun.btrace.client.Client
- compiler方法 : 调用了Java Complier Api进行动态编译你的Btrace.java文件
- this.compiler = ToolProvider.getSystemJavaCompiler();
- this.stdManager = this.compiler.getStandardFileManager(null, null, null);
- Verifier btraceVerifier = new Verifier(this.unsafe); //指定了btrace特定的语法校验器
- attach方法: 调用VirtualMachine.attach(pid);vm.loadAgent(agentPath, agentArgs);动态加载btrace-agent.jar包
- 传递给agent程序的几个参数:
- debug=true
- unsafe=true
- dumpClass=true
- dumpDir=xx
- trackRetransforms=true ##是否记录instrument行为
- bootClassPath= xx ##agent.jar使用
- systemClassPath =xx ##agent.jar使用
- probeDescPath=xx
- submit方法: 调用提交对应的instrument指令,并传递对应code的byte[]
- this.sock = new Socket("localhost", this.port);
- this.oos = new ObjectOutputStream(this.sock.getOutputStream());
- ...
- WireIO.write(this.oos, new InstrumentCommand(code, args));
几点说明:
* 在调用了attach方法后,会通过btrace-agent.jar中的com.sun.btrace.agent.Main启动一个ServerSocket
- int port = 2020;
- String p = (String)argMap.get("port");
- ....
- ServerSocket ss;
- try {
- (isDebug()) debugPrint(new StringBuilder().append("starting server at ").append(port).toString());
- System.setProperty("btrace.port", String.valueOf(port));
- if ((scriptOutputFile != null) && (scriptOutputFile.length() > 0)) {
- System.setProperty("btrace.output", scriptOutputFile);
- }
- ss = new ServerSocket(port);
- } catch(Exception e) ....
- while (true)
- {
- if (!isDebug()) continue; debugPrint("waiting for clients");
- Socket sock = ss.accept();
- if (!isDebug()) continue; debugPrint(new StringBuilder().append("client accepted ").append(sock).toString());
- Client client = new RemoteClient(inst, sock);
- handleNewClient(client);
- continue;
- }
* 所以在submit中,会通过一个本地socket进行连接server,并提交相应的Btrace.java中的监控代码(这时应该是编译后的字节码).
3. com.sun.btrace.compiler.Verifier btrace自定义的语法校验器
- Boolean value = this.unsafe ? Boolean.TRUE : (Boolean)ct.accept(new VerifierVisitor(this), null); // 注意下unsafe的判断
4. com.sun.btrace.compiler.VerifierVisitor (具体的一些检查规则)
- visitBinary String字符串的+限制
- visitClass class的检查,不允许有父类,不允许有接口类,不允许非static变量,必须有Btrace @标签
- visitDoWhileLoop 不允许do while循环
- visitForLoop 不允许for循环
- visitMethod 必须为static public ,不允许出现synchronized标记
- visitNewArray 不允许出现new Array
- visitNewClass 不允许出现new 对象
- visitReturn 不允许有返回值
- visitSynchronized 不允许有同步快
- visitThrow visitTry 不允许有try catch的动作
- visitOther 除上面允许之外的,不允许有其他的
说明:
* 看完Verifier和VerifierVisitor后,相信大家都应该明白了Btrace所谓的诸多限制,只是针对.java需要动态编译。如果我们预先生成.class文件,Btrace在1.2版本中并不会作类型合法性检查。(在将code发送给btrace-agent后,会在目标的jvm内部进行一次简单的Btrace语法检查,具体见后面Btrace-agent介绍)
5. com.sun.btrace.comm.XXX Btrace的各种command指令
btrace-agent
大致了解了Client类中的attach和submit方法后,相信也能猜到对应agent的一些设计。简单的看一下
1. com.sun.btrace.agent.Main 为attach上之后agent的总入口,会调用agentmain()方法
- main方法: 首先解析参数,然后会启动一个agentThread(Daemon线程)
- parseArgs : 对应的参数解析,客户端在attach时,提交给agent后的一些参数
- bootClassPath=xx.jar //需要动态增加的jar
- systemClassPath=xx.jar
- noServer=true/false //是否启动server socket
- debug=true/false
- unsafe=true/false
- dumpClasses=true/false
- dumpDir=路径
- trackRetransforms=true/false
- probeDescPath=路径
- stdout=true/false
- script=文件
- scriptdir=路径
- scriptOutputFile=文件路径特别注意下相比于Btrace-client提交的参数中,多了几个script,scriptdir等参数,允许在Client调用服务端一个指定的Btrace script文件进行处理
- loadBTraceScript : 装载指定的script(注意是script和scriptDir中指定的script),必须是.class文件,会调用FileClient进行处理,最后调用handleNewClient进行统一处理,最后调用handleNewClient进行统一instrument处理。
- startServer : main启动的agentThread会调用该方法,这里会启动一个serversocket,和btrace-client的客户端socket进行通讯,使用RemoteClient,最后调用handleNewClient进行统一instrument处理。
- handleNewClient : 启动一个异步线程进行class Transformer,根据提交的byte[] code进行类文件重写
说明:
* 目前instrument进行字节码重写时,会重新load所有的class进行处理。(Btrace可以使用正则,父类的方式进行匹配,只能是挨个Class进行处理,看下是否有匹配的OnMethod)
* 相比于btrace-client提交过来的参数中,btrace-agent支持的参数中多了几个script,scriptdir等,允许在Client调用服务端一个指定的Btrace script文件进行处理,注意这里的script必须是编译后的.class文件。和通过socket提交的btrace在处理上没有太大的差异。
2. com.sun.btrace.agent.RemoteClient / FileClient : (RemoteClient为通过socket提交的script , FileClient为script和scriptDir指定的script文件)
- 两者的不同无非就是对应的结果输出方式不同,一个是传回给客户端,另一个是直接终端输出
- 会调用Client.loadClass()进行btrace数据解析,主要是解析对应的OnMethod和OnProbe数据。
3. com.sun.btrace.agent.ProbeDescriptorLoader
- 会解析对应Btrace script中出现的@OnProbe,解析xml文件中对应的@OnMethod信息
4. com.sun.btrace.agent.Client: (RemoteClient和FileClient的共同父类)
- instument中的Transformer的实现类
- loadClass()方法: btrace script脚本解析
- verify 首先进行class的校验, 调用Verity类进行检查(可手工执行:java com.sun.btrace.runtime.Verifier <.class file>)
- runtime.defineClass(codeBuf); 使用反射重新装载class byte
- transform: 核心的方法(isBTraceClass(cname)) || (isSensitiveClass(cname)) 过滤btrace内部类,已经一些Object,ThreadLocal,sun/reflect类)
- instrument方法 : 方法中调用asm的ClassReader进行class对象解析,并设置Instrumentor进行Class对象处理
5. com.sun.btrace.runtime.Instrumentor : 是Btrace实现代码监控增强处理的核心逻辑
可以直接调用:
- java com.sun.btrace.runtime.Instrumentor <btrace-class> <target-class>]
Btrace的几点总结
1. btrace支持的监控方式
- 本地jvm监控:目前大多数都是用的是btrace和监控的目标java是在同一机器上
- 远程jvm监控:需要在远程服务器启动时添加btrace-agent.jar,需要重写btrace客户端,完成和serversocket建立通讯,完成btrace script发送监控。
1. VirtualMachine动态attach不支持远程操作,所以无法动态的进行agent添加。
2. btrace支持的jdk版本
- java 1.4以及之前 : 不支持,Instrument在jdk 1.5之后才出现。
- java 1.5 : 必须手动在jvm启动时添加btrace-agent.jar,因为VirtualMachine是在jdk 1.6之后才出现。
- java 1.6 : 推荐使用
agent启动:
- java -Xshare:off -javaagent:${BTRACE_HOME}/build/btrace-agent.jar=dumpClasses=false,debug=false,unsafe=false,probeDescPath=.,noServer=true,script=$1
- 具体的参数见上面的代码分析
3. btrace的支持的script方式有多种。
- client上的.java文件
会进行动态编译,会有比较多的语法限制,btrace一堆的你不能做的事
- client上的.class文件
没什么好讲的,自己写Btrace script时导入btrace-client.jar,写好后生成一个.class文件,再通过btrace pid Btrace.class进行启动。
- remote上的.class文件
1. 修改btrace-client中的Client类,支持script和scriptDir的一些参数提交。
2. 在remote机器上放置对应的btrace.class文件
4. btrace的使用是否会对java进程造成影响?(影响是肯定的,不过影响不大)
装载时的影响:
- btrace每次使用,都会重新load所有的class。当然如果OnMethod不匹配,是不会被重新装载。所以跟你的OnMethod的匹配规则很有关系,如果使用+java.lang.Object。那就死定了。
退出后的影响:
- btrace监控每次退出后,原先所有的class都不会被恢复,你的所有的监控代码依然一直在运行
抓取了下btrace改写过后的类:
- public InstrumentServer(String ip, String port)
- {
- $btrace$com$agapple$btrace$Instrumentor$InstrumentTracer$bufferMonitor(this);
- this.ip = ip;
- this.port = port;
- }
- private static void $btrace$com$agapple$btrace$Instrumentor$InstrumentTracer$bufferMonitor(@Self Object arg0)
- {
- if (!BTraceRuntime.enter(InstrumentTracer.runtime)) return; try { Field ipField = BTraceUtils.field("com.agapple.btrace.Instrumentor.InstrumentServer", "ip");
- Field portField = BTraceUtils.field("com.agapple.btrace.Instrumentor.InstrumentServer", "port");
- String ip = (String)BTraceUtils.get(ipField, self);
- String port = (String)BTraceUtils.get(portField, self);
- BTraceUtils.println(BTraceUtils.strcat(BTraceUtils.strcat(BTraceUtils.strcat("ip : ", BTraceUtils.str(ip)), " port : "), BTraceUtils.str(port)));
- BTraceRuntime.leave(); return; } catch (Throwable localThrowable) { BTraceRuntime.handleException(localThrowable);
- }
- }
注意其中的if (!BTraceRuntime.enter(InstrumentTracer.runtime)) return;
再看一下BTraceRuntime中对应方法的实现:
- private volatile boolean disabled;
- public static boolean enter(BTraceRuntime current)
- {
- if (current.disabled) return false;
- return map.enter(current);
- }
每次执行你的监控代码之前会先进行一个判断,判断当前是否处于监控中。你的客户端发起了exit指令后,该方法判断false,直接return。
所以btrace使用退出后会让你的代码多走了一个方法调用+一个对象属性判断,所以说影响还是非常的少
5. btrace诸多的使用限制,你必须得知道:
- 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 or methods declared in the same program may be called from a BTrace program.
- (pre 1.2) 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 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 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有这诸多的限制,才可以让我们的监控代码可以更加的放心,这也正是btrace能普及的一个很重要的原因。
- 不得不说的一个点:对String的"+"限制使用,让我们使用起来很不爽,不过还好在btrace 1.2之后,作者提供了一个StringBuilder。相比于strcat已经好用多了
6. btrace对string字符串的处理
- 可以参看总结3,突破对应的限制。不是非常建议,因为总结4中提出即使btrace client退出后,服务端一直会运行btrace script。所以一旦有写的动作,会是一个长期持续的过程
- btrace 1.2 release说明中,已经提到增加了StringBuilder进行字符串处理,至少比先前的strcat使用上已经方便很多了。具体查看:http://kenai.com/jira/browse/BTRACE-38
7. btrace的相关源码:
8. btrace中对OnMethod的Location使用上,以及一些annotation使用不明确,可以查看:http://kenai.com/projects/btrace/sources/hg/content/src/share/classes/com/sun/btrace/runtime/Instrumentor.java
说明: self, ProbeClassName , ProbeMethodName 在任何的Kind中都支持,所以就不在每个表格中赘述。
Kind |
Where.BEFORE |
Where.AFTER |
ARRAY_GET |
数组长度(int) , 数组类型(type) |
@return , 数组长度(int) , 数组类型(type) |
ARRAY_SET |
原始数组类型(type) , 数组长度(int) , 目标数组类型(type) |
@return,原始数组类型(type) , 数组长度(int) , 目标数组类型(type) |
CALL |
方法参数 , @TargetInstance , @TargetMethodOrField |
方法参数, @return , @TargetInstance , @TargetMethodOrField |
CATCH |
异常类型(type) |
异常类型(type) |
CHECKCAST |
转型的目标类型 |
转型的目标类型 |
ENTRY |
方法参数 |
方法参数 |
ERROR |
异常类型(throwable type) |
异常类型(throwable type) |
FIELD_GET |
@TargetInstance,@TargetMethodOrField |
@TargetInstance,@TargetMethodOrField,@return |
FIELD_SET |
fldValueIndex,@TargetInstance,@TargetMethodOrField |
fldValueIndex,@TargetInstance,@TargetMethodOrField |
INSTANCEOF |
转型的目标类型 |
转型的目标类型 |
LINE |
行数 |
行数 |
NEW |
对象类名 |
@return |
NEWARRAY |
数组内部对象类名,类名 |
数组内部对象类名,类名, @return |
RETURN |
无 |
参数,@return , @Duration |
SYNC_ENTRY |
sync对象 |
sync对象 |
SYNC_EXIT |
sync对象 |
sync对象 |
THROW |
异常类型 |
异常类型 |
最后
花了多个小时时间整理了这份blog,希望能给大家理解btrace,掌握btrace的使用能带来一些帮助!!
有问题和交流,欢迎站内联系