BTrace 是一款利用hotSpot虚拟机可以动态替换class的特点而完成的,可以对online的程序动态的改变类的行为(一般为加些打印日志),进而进行线上调试的一个工具。
一篇淘宝技术团队的博客:http://rdc.taobao.com/team/jm/archives/509
主要步骤如下(本次测试只针对BTrace和测试的程序在同一台机器上,remote的还待实验):
1、下载地址:http://kenai.com/projects/btrace/downloads/download/releases/release-1.2.2/btrace-bin.zip
2、解压到linux相应的目录下。
3、编写普通运行的程序如下:
package com.ddc.mem; public class CaseObject{ private static int sleepTotalTime=0; public boolean execute(int sleepTime) throws Exception{ System.out.println("sleep: "+sleepTime); sleepTotalTime+=sleepTime; Thread.sleep(sleepTime); if(sleepTime%2==0) return true; else return false; } }
Main函数运行:
package com.ddc.mem; import java.util.Random; public class CaseObjectMain { public static void main(String[] args) throws Exception{ Random random=new Random(); CaseObject object=new CaseObject(); while(true){ boolean result=object.execute(random.nextInt(1000)); Thread.sleep(1000); } } }
4、将以上两个类编译并运行 。
5、编写BTrace 文件,按照java规范,java文件名称为TracingScript.java
/* BTrace Script Template */ import com.sun.btrace.annotations.*; import static com.sun.btrace.BTraceUtils.*; @BTrace public class TracingScript { /* put your code here */ /*指明要查看的方法,类*/ @OnMethod( clazz="com.ddc.mem.CaseObject", method="execute", location=@Location(Kind.RETURN) ) /*主要两个参数是对象自己的引用 和 返回值,其它参数都是方法调用时传入的参数*/ public static void traceExecute(@Self com.ddc.mem.CaseObject object,int sleepTime, @Return boolean result){ println("调用堆栈!!"); println(strcat("返回结果是:",str(result))); jstack(); println(strcat("时间是:",str(sleepTime))); } }
6、对于btrace文件夹加运行时路径(java_home 和 classpath)
修改{btrace_home}/bin/btrace 文件
#! /bin/sh #需要设置jdk的路径,因为需要动态编译,所以需要设置这个路径 JAVA_HOME=/usr/lib/jvm/java-6-sun-1.6.0.22 #因为需要动态编译,所以需要设置原类库的classpath,主要是要编译BTRACE文件,它里面肯定有依赖原类 CLASS_PATH=/data/testxiao/ #BTRACE_HOME路径,编译以及运行时都需要BTRACE自己的jar包 BTRACE_HOME=/data/btrace if [ -z "$BTRACE_HOME" -o ! -d "$BTRACE_HOME" ] ; then # resolve links - $0 could be a link to btrace's home PRG="$0" progname=`basename "$0"` BTRACE_HOME=`dirname "$PRG"`/.. BTRACE_HOME=`cd "$BTRACE_HOME" && pwd` fi if [ -f "${BTRACE_HOME}/build/btrace-client.jar" ] ; then if [ "${JAVA_HOME}" != "" ]; then case "`uname`" in Darwin*) # In Mac OS X, tools.jar is classes.jar and is kept in a # different location. Check if we can locate classes.jar # based on ${JAVA_VERSION} TOOLS_JAR="/System/Library/Frameworks/JavaVM.framework/Versions/${JAVA_VERSION}/Classes/classes.jar" # if we can't find, try relative path from ${JAVA_HOME}. Usually, # /System/Library/Frameworks/JavaVM.framework/Versions/1.6.0/Home # is JAVA_HOME. (or whatever version beyond 1.6.0!) if [ ! -f ${TOOLS_JAR} ] ; then TOOLS_JAR="${JAVA_HOME}/../Classes/classes.jar" fi # If we still can't find, tell the user to set JAVA_VERSION. # This way, we can avoid zip file errors from the agent side # and "connection refused" message from client. if [ ! -f ${TOOLS_JAR} ] ; then echo "Please set JAVA_VERSION to the target java version" exit 1 fi ;; *) TOOLS_JAR="${JAVA_HOME}/lib/tools.jar" ;; esac ${JAVA_HOME}/bin/java -Dcom.sun.btrace.probeDescPath=. -Dcom.sun.btrace.dumpClasses=false -Dcom.sun.btrace.debug=false -Dcom.sun.btrace.unsafe=false -cp ${BTRACE_HOME}/build/btrace-client.jar:${TOOLS_JAR}:/usr/share/lib/java/dtrace.jar:${CLASS_PATH} com.sun.btrace.client.Main $* else echo "Please set JAVA_HOME before running this script" exit 1 fi else echo "Please set BTRACE_HOME before running this script" exit 1 fi
7、jps CaseObjectMain进程的pid,假设pid为1478 ,刚才的btrace 为TracingScript.java
则运行命令为 :bin/btrace 1478 TracingScript.java
8、一切ok
输出如下:
调用堆栈!!
返回结果是:false
com.ddc.mem.CaseObject.execute(CaseObject.java:14)
com.ddc.mem.CaseObjectMain.main(CaseObjectMain.java:11)
时间是:483
调用堆栈!!
返回结果是:true
com.ddc.mem.CaseObject.execute(CaseObject.java:12)
com.ddc.mem.CaseObjectMain.main(CaseObjectMain.java:11)
时间是:998
调用堆栈!!
返回结果是:false
com.ddc.mem.CaseObject.execute(CaseObject.java:14)
com.ddc.mem.CaseObjectMain.main(CaseObjectMain.java:11)
时间是:611
调用堆栈!!
返回结果是:true
com.ddc.mem.CaseObject.execute(CaseObject.java:12)
com.ddc.mem.CaseObjectMain.main(CaseObjectMain.java:11)
时间是:524
表明其实已经改变了原有的输出并加上了打印日志 。
BTrace本身也是可以独立运行的程序,作用是在不停止目标程序运行的前提下,通过HotSpot虚拟机的HotSwap技术动态插入原本不存在的调试代码。
比如遇到了我们的程序出问题,而又没有足够的打印语句时,我们一般的方法是不得不停掉服务,然后修改代码,增加打印语句,重新编译重新运行来解决,效率很低。
但有了BTrace,我们需要做的就很简单了,举例说明:
比如环境上运行着一个简单程序:
package com.huawei.main;
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class Main
{
public static void main(String[] args) throws Exception
{
Main test = new Main();
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
for (int i = 0; i < 10; i++)
{
reader.readLine();
int a = (int) Math.round(Math.random() * 1000);
int b = (int) Math.round(Math.random() * 1000);
System.out.println(test.add(a, b));
}
}
public int add(int a, int b)
{
return a + b;
}
}
该程序从控制台中获取一个输入,然后生成两个随机数,相加后将结果打印出来
对于add方法没有日志打印,如果想在不改变程序的前提下知道程序运行时add函数的入参和返回值,我们可以:
1. 在环境上解压BTrace工具包
比如解压到:/opt/eucalyptus/test/目录下
2. 编写BTrace脚本,对于脚本还是需要时间学习和实践的。如下TraceScript.java(注意在Linux下,这个文件应该是ANSI格式,否则会报illegal character: \65279的异常):
import com.sun.btrace.annotations.*;
import static com.sun.btrace.BTraceUtils.*;
@BTrace
public class TraceScript
{
@OnMethod(clazz="com.huawei.main.Main", method="add", location=@Location(Kind.RETURN))
public static void func(int a, int b, @Return int result)
{
jstack();
println(strcat("para A: ", str(a)));
println(strcat("para B: ", str(b)));
println(strcat("result: ", str(result)));
}
}
将该脚本放在环境目录下,比如:/opt/eucalyptus/test/TraceScript.java
test目录结构如下:
除TraceScript.java外都是BTrace解压后的文件。
3. 利用jps得到Main程序的进程号(比如28772),到BTrace目录的bin目录下执行语句:
/opt/eucalyptus/test/bin # ./btrace 28772 ../TraceScript.java
4. 在Main程序的控制台下输入字符,回车,会看到BTrace的输出:
总结:BTrace用法还有很多,打印调用堆栈、参数、返回值只是最基本的应用,在BTrace网址上有使用BTrace进行性能监视、定位连接泄露、内存泄露、解决多线程竞争问题等例子。
注意:如果一个java程序是以普通用户权限运行,则不能在root权限下对其进行btrace,一定先要切换到普通用户。
BTrace源码的查探点一般是类级别的,指定类名后,这个设置将对此类的所有对象都生效。
但有时我们只想监控这个类里的某个对象,比如某个业务类中的某个HashSet类型的成员变量。
BTrace对此没有直接的支持,但有的情况下你可以考虑一种变通的方法:
1. 先写一个BTrace源码找到你要访问的特定对象的hashCode.
- int hashCode = BTraceUtils.identityHashCode(set)
2. 然后另写BTrace源码,按hashCode过滤对象:
- @OnMethod(clazz="java.util.HashSet", method="/.*/")
- public static void allListMethodsEntry(@Self java.util.HashSet set) throws Exception {
- if(identityHashCode(set) == hashCode ) {
- ...
- }
- }
不得不承认,这种办法的适用场景非常有限:
你的目标应该是一个长寿对象(比如系统中的单例),这个对象的hashCode一直不变。