一、简介:

在生产环境中经常遇到格式各样的问题,如OOM或者莫名其妙的进程死掉。一般情况下是通过修改程序,添加打印日志;然后重新发布程序来完成。然而,这不仅麻烦,而且带来很多不可控的因素。有没有一种方式,在不修改原有运行程序的情况下获取运行时的数据信息呢?如方法参数、返回值、全局变量、堆栈信息等。Btrace就是这样一个工具,它可以在不修改原有代码的情况下动态地追踪java运行程序,通过hotswap技术,动态将跟踪字节码注入到运行类中,对运行代码侵入较小,对性能上的影响可以忽略不计。

在下列情况时可以使用BTrace进行分析:

1、接口性能变慢,分析每个方法的耗时情况;

2、当在Map中插入大量数据,分析其扩容情况;

3、分析哪个方法调用了System.gc(),调用栈如何;

4、执行某个方法抛出异常时,分析运行时参数;

5、..................

二、安装:

1、安装JDK;

2、下载BTrace的压缩包,这里使用的是BTrace 1.3.11版本,可以到下面地址下载:

http://www.voidcn.com/link?url=https://github.com/btraceio/btrace/releases/tag/v1.3.11

3、将BTrace包解压,在系统的环境变量上添加变量BTRACE_HOME,并设置其路径为BTrace的路径,同时在PATH变量中添加上路径%BTRACE_HOME%\bin;

4、编辑%BTRACE_HOME%\bin\btrace.bat文件,将其中的-Dcom.sun.btrace.unsafe=false改为-Dcom.sun.btrace.unsafe=true;

5、btrace命令的语法说明:

btrace [-I <include-path>] [-p <port>] [-cp <classpath>] <pid> <btrace-script> [<args>]

1)没有这个表明跳过预编译;

2)include-path:指定用来编译脚本的头文件路径(关于预编译可参考例子ThreadBean.java);

3)port:btrace agent端口,默认是2020;

4)classpath:编译所需类路径,一般是指btrace-client.jar等类所在路径;

5)pid:java进程id;

6)btrace-script:btrace脚本可以是.java文件,也可以是.class文件;

7)args:传递给btrace脚本的参数, 在脚本中可以通过$(), $length()来获取这些参数(定义在BTraceUtils中);

三、Demo

(一)JavaSE应用Demo

1、编写测试功能实现类:

public class Calculator {

public int add(int a, int b) {

try {

Thread.sleep(1000);

} catch (InterruptedException e) {

e.printStackTrace();

}

return a + b;

}

}

2、编写调用代码:

public class App

{

public static void main( String[] args )

{

Calculator calc = new Calculator();

Random random = new Random();

while (true) {

int a = random.nextInt(10);

int b = random.nextInt(20);

int c = calc.add(a, b);

System.out.println(String.format("%d + %d = %d", a, b, c));

}

}

}

上面的代码无限循环调用Calculator .add方法并输出调用结果;

3、运行程序,可以看到屏幕上不停的输出各种加法运算的表达式;

4、编写btrace脚本:

@BTrace(unsafe = true)

public class BTraceTest {

@OnMethod(clazz = "com.ucar.test.Calculator", method = "add", location = @Location(Kind.RETURN))

public static void traceTest(int a, int b, @Return int sum) {

println(String.format("%d + %d = %d", a, b, sum));

}

}

@BTrace注解中要加上unsafe=true,否则运行btrace脚本时会因为安全机制导致报错而无法执行脚本;

@OnMethod注解中的clazz表示要跟踪的类名,method表示要跟踪的方法名称,location表示在什么时候进行拦截;

5、运行btrace脚本,可以看到前面输出的加法运算表达式也能在这个窗口上输出;

运行btrace脚本的命令为:

btrace 3856 BTraceTest.java

其中3856为刚才运行的java程序的进程ID;

(二)web应用Demo

1、新建SpringMVC的web应用程序(参考https://www.cnblogs.com/laoxia/p/9311442.html);

2、实现Controller:

@RestController

@RequestMapping("/btrace")

public class BTraceController {

@RequestMapping("/arg1")

public String arg1(@RequestParam("name") String name) {

return "hello: " + name;

}

}

3、生成war包并放到tomcat的webapp目录下,启动tomcat,浏览器中打开URL地址:http://localhost:8080/test/btrace/arg1?name=aaaaa,页面上应该能正常打印出“hello: aaaaa”;

4、编写BTrace脚本:

@BTrace(unsafe = true)

public class PrintArgSimple {

@OnMethod(clazz = "com.ucar.test.controller.BTraceController", method = "arg1", location = @Location(Kind.RETURN))

public static void anyRead(@ProbeClassName String pcn, // 被拦截的类名

@ProbeMethodName String pmn,  //被拦截的方法名

AnyType[] args  //被拦截的方法的参数值) {

BTraceUtils.printArray(args);

BTraceUtils.println("className: " + pcn);

BTraceUtils.println("MethodName: " + pmn);

BTraceUtils.println();

}

}

注意:需要在maven的POM文件中引入btrace-client.jar, btrace-boot.jar和btrace_agent.jar三个文件或者直接引入这三个jar包;

5、运行脚本,然后在浏览器中请求第三步的URL地址,这时候就能看到屏幕上打印出运行过程中的相关信息;

运行BTrace的命令为:

btrace 1256 PrintArgSimple.java

其中1256为这个web应用进程的进程ID;

6、注意:需要将web应用打包成war放到tomcat下运行,如果直接在idea下运行会报错;

四、拦截时机:

1、Kind.ENTRY:入口拦截,默认值;

2、Kind.RETURN:拦截返回值,只有把拦截位置定义为Kind.RETURN,才能获取方法的返回结果@Return和执行时间@Duration;

3、Kind.THROW:发生异常时拦截;

4、Kind.LINE:拦截某一行,可以监控代码是否执行到指定的位置;

5、Kind.CALL:分析方法中调用其它方法的执行情况,比如在execute方法中,想获取add方法的执行耗时,必须把where设置成Where.AFTER;

五、技巧:

1、拦截构造函数:

指定method = "<init>"即可拦截指定类的构造函数;

2、拦截同名函数:拦截同名重载方法,只需要在BTrace脚本的方法中声明与之对应的参数即可。

比如有如下两个同名方法:

@RequestMapping("/same1")

public String same(@RequestParam("name") String name) {

return "hello: " + name;

}

@RequestMapping("/same2")

public User same(@RequestParam("id") int id,

@RequestParam("name") String name) {

return new User(id, name);

}

编写如下的btrace脚本即可拦截:

@OnMethod(clazz = "org.zero01.monitor_tuning.controller.BTraceController", method = "same")

public static void anyRead(@ProbeClassName String pcn, @ProbeMethodName String pmn, String name) {

BTraceUtils.println("ClassName: " + pcn);

BTraceUtils.println("MethodName: " + pmn);

BTraceUtils.println("name: " + name);

BTraceUtils.println();

}

@OnMethod(clazz = "org.zero01.monitor_tuning.controller.BTraceController", method = "same")

public static void anyRead(@ProbeClassName String pcn, @ProbeMethodName String pmn, int id, String name) {

BTraceUtils.println("ClassName: " + pcn);

BTraceUtils.println("MethodName: " + pmn);

BTraceUtils.println("id: " + id);

BTraceUtils.println("name: " + name);

BTraceUtils.println();

}

3、拦截返回值:

指定location=@Location(Kind.RETURN),并且在方法的参数里面加上@Return AnyType result即可接收返回值;

4、拦截异常:

@BTrace

public class PrintOnThrow {

@TLS

static Throwable currentException;

@OnMethod(

clazz="java.lang.Throwable",

method="<init>"

)

public static void onthrow(@Self Throwable self) {  // @Self其实就是拦截了this

currentException = self;

}

@OnMethod(

clazz="java.lang.Throwable",

method="<init>"

)

public static void onthrow1(@Self Throwable self, String s) {

currentException = self;

}

@OnMethod(

clazz="java.lang.Throwable",

method="<init>"

)

public static void onthrow1(@Self Throwable self, String s, Throwable cause) {

currentException = self;

}

@OnMethod(

clazz="java.lang.Throwable",

method="<init>"

)

public static void onthrow2(@Self Throwable self, Throwable cause) {

currentException = self;

}

@OnMethod(

clazz="java.lang.Throwable",

method="<init>",

location=@Location(Kind.RETURN)

)

public static void onthrowreturn() {

if (currentException != null) {

// 打印异常堆栈

BTraceUtils.Threads.jstack(currentException);

BTraceUtils.println("=====================");

// 打印完之后就置空

currentException = null;

}

}

}

在命令行里运行该脚本,访问相应的接口后,即可输出异常堆栈;即使异常被try catch给隐藏起来了,这个脚本也一样能揪出来。

5、拦截指定行:

@BTrace

public class PrintLine {

@OnMethod(

clazz="org.zero01.monitor_tuning.controller.BTraceController",

method="exception",

location=@Location(value=Kind.LINE, line=43)  // 拦截第43行

)

public static void anyRead(@ProbeClassName String pcn, @ProbeMethodName String pmn, int line) {

BTraceUtils.println("ClassName: " + pcn);

BTraceUtils.println("MethodName: " + pmn);

BTraceUtils.println("line: " + line);

BTraceUtils.println();

}

}

如果没有任何输出的话,就代表那一行没有被执行到,所以没被拦截。这种拦截某一行的方式,不适用于判断是否有异常,只能单纯用于判断某一行是否被执行了。

6、拦截复杂参数:

比如要拦截下面方法的复杂参数类型User:

@RequestMapping("/arg2")

public User arg2(User user) {

return user;

}

可以使用下面的btrace脚本拦截:

@BTrace

public class PrintArgComplex {

@OnMethod(

clazz = "org.zero01.monitor_tuning.controller.BTraceController",

method = "arg2",

location = @Location(Kind.ENTRY)

)

public static void anyRead(@ProbeClassName String pcn, @ProbeMethodName String pmn, User user) {

//print all fields

BTraceUtils.print("print all fields: ");

BTraceUtils.printFields(user);

//print one field

Field oneFiled = BTraceUtils.field("org.zero01.monitor_tuning.vo.User", "name");

BTraceUtils.println("print one field: " + BTraceUtils.get(oneFiled, user));

BTraceUtils.println("ClassName: " + pcn);

BTraceUtils.println("MethodName: " + pmn);

BTraceUtils.println();

}

}

7、拦截环境变量:

@BTrace

public class PrintJinfo {

static {

// 打印系统属性

BTraceUtils.println("System Properties:");

BTraceUtils.printProperties();

// 打印JVM参数

BTraceUtils.println("VM Flags:");

BTraceUtils.printVmArguments();

// 打印环境变量

BTraceUtils.println("OS Enviroment:");

BTraceUtils.printEnv();

// 退出脚本

BTraceUtils.exit(0);

}

}

8、使用正则表达式拦截:

@BTrace

public class PrintRegex {

@OnMethod(

// 类名也可以使用正则表达式进行匹配

clazz = "org.zero01.monitor_tuning.controller.BTraceController",

// 正则表达式需要写在两个斜杠内

method = "/.*/"

)

public static void anyRead(@ProbeClassName String pcn, @ProbeMethodName String pmn) {

BTraceUtils.println("ClassName: " + pcn);

BTraceUtils.println("MethodName: " + pmn);

BTraceUtils.println();

}

}

六、注意事项:

1、@ProbeClassName String clazz:此处String不能写为java.lang.String;

2、@OnMethod(clazz="com.alibaba.security.acl.support.PermissionFactory", method="createPermission", type="com.alibaba.security.acl.support.AbstractPermission(java.lang.String,java.lang.String,com.alibaba.security.acl.support.PermissionDefiner)")

此处得String必须写成java.lang.String;

3、BTrace脚本默认只能本地运行,也就是只能调试本地的Java进程。如果需要在本地调试远程的Java进程的话,是需要自己去修改BTrace源码的;

4、BTrace脚本在生产环境下可以使用,但是被修改的字节码不会被还原。所以我们需要先在本地调试好BTrace脚本,然后才能放到生产环境下使用。并且需要注意BTrace脚本中不能含有影响性能或消耗资源较多的代码,不然会导致线上的服务性能降低。

七、其他:

1、其他命令行工具说明:

(1) Btracec:用于预编译BTrace脚本,用于在编译时期验证脚本正确性。

btracec [-I <include-path>] [-cp <classpath>] [-d <directory>] <one-or-more-BTrace-.java-files>

参数意义同btrace命令一致,directory表示编译结果输出目录。

(2) Btracer:btracer命令同时启动应用程序和BTrace脚本,即在应用程序启动过程中使用BTrace脚本。而btrace命令针对已运行程序执行BTrace脚本。

btracer <pre-compiled-btrace.class> <application-main-class> <application-args>

参数说明:

pre-compiled-btrace.class表示经过btracec编译后的BTrace脚本。

application-main-class表示应用程序代码;

application-args表示应用程序参数。

2、方法上的注解:

(1) @ OnMethod用来指定trace的目标类和方法以及具体位置,被注解的方法在匹配的方法执行到指定的位置会被调用。

  • "clazz"属性用来指定目标类名,可以指定全限定类名,比如"java.awt.Component",也可以是正则表达式(表达式必须写在"//"中,比如"/java\\.awt\\..+/")。
  • "method"属性用来指定被trace的方法.表达式可以参考自带的例子(NewComponent.java和Classload.java,关于方法的注解可以参考MultiClass.java)。
  • 有时候被trace的类和方法可能也使用了注解.用法参考自带例子WebServiceTracker.java。
  • 针对注解也是可以使用正则表达式,比如像这个"@/com\\.acme\\..+/ ",也可以通过指定超类来匹配多个类,比如"+java.lang.Runnable"可以匹配所有实现了java.lang.Runnable接口的类.具体参考自带例子SubtypeTracer.java。

(2) @OnTimer定时触发Trace,时间可以指定,单位为毫秒,具体参考自带例子Histogram.java。

(3) @OnError当trace代码抛异常或者错误时,该注解的方法会被执行.如果同一个trace脚本中其他方法抛异常,该注解方法也会被执行。

(4) @OnExit当trace方法调用内置exit(int)方法(用来结束整个trace程序)时,该注解的方法会被执行.参考自带例子ProbeExit.java。

(5) @OnEvent用来截获"外部"btrace client触发的事件,比如按Ctrl-C中断btrace执行时,并且选择2,或者输入事件名称,将执行使用了该注解的方法,该注解的value值为具体事件名称。具体参考例子HistoOnEvent.java;

(6) @OnLowMemory当内存超过某个设定值将触发该注解的方法,具体参考MemAlerter.java;

(7) @OnProbe使用外部文件XML来定义trace方法以及具体的位置,具体参考示例SocketTracker1.java和java.net.socket.xml。

3、参数上的注解:

  • @Self用来指定被trace方法的this,可参考例子AWTEventTracer.java和AllCalls1.java
  • @Return用来指定被trace方法的返回值,可参考例子Classload.java
  • @ProbeClassName (since 1.1)用来指定被trace的类名,可参考例子AllMethods.java
  • @ProbeMethodName (since 1.1)用来指定被trace的方法名,可参考例子WebServiceTracker.java。
  • @TargetInstance (since 1.1)用来指定被trace方法内部被调用到的实例,可参考例子AllCalls2.java
  • @TargetMethodOrField (since 1.1)用来指定被trace方法内部被调用的方法名,可参考例子AllCalls1.java和AllCalls2.java。

4、属性上的注解:

  • @Export该注解的静态属性主要用来与jvmstat计数器做关联, 使用该注解之后,btrace程序就可以向jvmstat客户端(可以用来统计jvm堆中的内存使用量)暴露trace程序的执行次数, 具体可参考例子ThreadCounter.java。
  • @Property使用了该注解的trace脚本将作为MBean的一个属性,一旦使用该注解, trace脚本就会创建一个MBean并向MBean服务器注册, 这样JMX客户端比如VisualVM, jconsole就可以看到这些BTrace MBean, 如果这些被注解的属性与被trace程序的属性关联, 那么就可以通过VisualVM和jconsole来查看这些属性了, 具体可参考例子ThreadCounterBean.java和HistogramBean.java。
  • @TLS用来将一个脚本变量与一个ThreadLocal变量关联, 因为ThreadLocal变量是跟线程相关的, 一般用来检查在同一个线程调用中是否执行到了被trace的方法, 具体可参考例子OnThrow.java和WebServiceTracker.java。

5、类上的注解:

  • @com.sun.btrace.annotations.DTrace用来指定btrace脚本与内置在其脚本中的D语言脚本关联, 具体参考例子DTraceInline.java。
  • @com.sun.btrace.annotations.DTraceRef用来指定btrace脚本与另一个D语言脚本文件关联, 具体参考例子DTraceRefDemo.java。
  • @com.sun.btrace.annotations.BTrace用来指定该java类为一个btrace脚本文件。

6、BTrace文件下的samples文件夹下包含了很多的示例,这些示例说明如下:

AWTEventTracer.java -演示了对EventQueue.dispatchEvent()事件进行trace的做法,可以通过instanceof来对事件进行过滤,比如这里只针对focus事件trace.

AllLines.java -演示了如何在被trace的程序到达probe指定的类和指定的行号时执行指定的操作(例子中指定的行号是-1表示任意行).

AllSync.java -演示了如何在进入/退出同步块进行trace.

ArgArray.java -演示了打印java.io包下所有类的readXXX方法的输入参数.

Classload.java -演示打印成功加载指定类以及堆栈信息.

CommandArg.java -演示如何获取btrace命令行参数.

Deadlock.java -演示了@OnTimer注解和内置deadlock()方法的用法

DTraceInline.java -演示@DTrace注解的用法

DTraceDemoRef.java -演示@DTraceRef注解的用法.

FileTracker.java -演示了如何对File{Input/Output}Stream构造函数中初始化打开文件的读写文件操作进行trace.

FinalizeTracker.java -演示了如何打印一个类所有的属性,这个在调试和故障分析中非常有用.这里的例子是打印FileInputStream类的close() /finalize()方法被调用时的信息.

Histogram.java -演示了统计javax.swing.JComponent在一个应用中被创建了多少次.

HistogramBean.java -同上例,只不过演示了如何与JMX集成,这里的map属性通过使用@Property注解被暴露成一个MBean.

HistoOnEvent.java -同上例,只不过演示了如何在通过按ctrl+c中断当前脚本时打印出创建次数,而不是定时打印.

JdbcQueries.java -演示了聚合(aggregation)功能.关于聚合功能可参考DTrace.

JInfo.java -演示了内置方法printVmArguments(), printProperties()和printEnv()的用法

JMap.java -演示了内置方法dumpHeap()的用法.即将目标应用的堆信息以二进制的形式dump出来

JStack.java -演示了内置方法jstackAll()的用法,即打印所有线程的堆栈信息.

LogTracer.java -演示了如何深入实例方法(Logger.log)并调用内置方法(field() )打印私有属性内容.

MemAlerter.java -演示了使用@OnLowMememory注解监控内存使用情况.即堆内存中的年老代达到指定值时打印出内存信息.

Memory.java -演示每隔4s打印一次内存统计信息.

MultiClass.java -演示了通过使用正则表达式对多个类的多个方法进行trace.

NewComponent.java -使用计数器每隔一段时间检查当前应用中创建java.awt.Component的个数.

OnThrow.java -当抛出异常时,打印出异常堆栈信息.

ProbeExit.java -演示@OnExit注解和内置exit(int)方法的用法

Profiling.java -演示了对profile的支持. //我执行没成功, BTrace内部有异常

Sizeof.java -演示了内置的sizeof方法的使用.

SocketTracker.java -演示了对socket的creation/bind方法的trace.

SocketTracker1.java -同上,只不过使用了@OnProbe.

SysProp.java -演示了使用内置方法获取系统属性,这里是对java.lang.System的getProperty方法进行trace.

SubtypeTracer.java -演示了如何对指定超类的所有子类的指定方法进行trace.

ThreadCounter.java -演示了在脚本中如何使用jvmstat计数器. (jstat -J-Djstat.showUnsupported=true -name btrace.com.sun.btrace.samples.ThreadCounter.count需要这样来从外部通过jstat来访问)

ThreadCounterBean.java -同上,只不过使用了JMX.

ThreadBean.java -演示了对预编译器的使用(并结合了JMX).

ThreadStart.java -演示了脚本中DTrace的用法.

Timers.java -演示了在一个脚本中同时使用多个@OnTimer

URLTracker.java -演示了在每次URL.openConnection成功返回时打印出url.这里也使用了D语言脚本.

WebServiceTracker.java -演示了如何根据注解进行trace.

7、参考文档:

http://huanghaifeng1990.iteye.com/blog/2121419

http://agapple.iteye.com/blog/962119

http://agapple.iteye.com/blog/1005918

05-11 02:44