一、简介:
在生产环境中经常遇到格式各样的问题,如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