BTrace 是 Java 的一个诊断工具,可以在不重启应用的情况下,对应用进行时间耗费、参数及结果跟踪、方法调用跟踪等分析。
BTrace 术语
Probe Point
位置
Trace Actions or Actions
追踪语句
Action Methods
追踪语句所在的静态方法
BTrace 程序结构
一个 BTrace 程序 是一个 Java 类,包含数个由 BTrace 注解
注释的
public static void
方法。这些注解被用来指定被追踪程序的 Probe
Point. Tracing Actions
在这些静态方法内定义。这些静态方法也即上文提到的 Action Methods。
BTrace 的限制
不能 创建对象,数组。
不能 抛出、捕获异常。
不能 调用任意实例或静态方法,只能调用 BTraceUtils 中的方法。
不能 修改目标程序的静态或实例变量,不过 BTrace 程序自己不做限制。
不能 有实例变量或方法,方法不能有返回值类型,BTrace 程序的所有方法必须是 public static 1. oid 的,所有的字段都必须是 static 的。
不能 有 outer, inner, nested 或 local 类。
不能 用 synchronized 关键字。
不能 有循环 (for, while, do..while)。
不能 继承任何类 (即父类只能是 java.lang.Object)。
不能 实现接口。
不能 包含 assert 语句.
不能 使用 class literals.
虽然不需要重启就可以使用 BTrace, 但是使用不当仍然会导致应用终止,比如目标程序可能会发生下面这种错误:
Exception in thread "main" java.lang.NoSuchMethodError: com.foo.btraceDemo.Target.$btrace$com$foo$btraceDemo$Tracer$func(Ljava/lang/Object;IJII)V
一个简单的 BTrace 程序
演示
Tracer.java — BTrace 要执行的程序(脚本).
// 导入所有 BTrace 注解。
import com.sun.btrace.annotations.*;
// 导入 BTraceUtils 的静态方法。
import static com.sun.btrace.BTraceUtils.*;
// @BTrace annotation tells that this is a BTrace program
@BTrace
public class HelloWorld {
// @OnMethod 注解表明 Probe Point(位置)。
// 在这个例子中,我们对进入 Thread.start() 方法感兴趣。
@OnMethod(
clazz="java.lang.Thread",
method="start"
)
public static void func() {
// println 是 BTraceUtils 的静态方法
println("about to start a thread!");
// System.out.println("123");
// new Tracer(); :
}
}
错误: Tracer.java:21:method calls are not allowed - only calls to BTraceUtils are allowed
错误: Tracer.java:22:object creation is not allowed
不要忘记 @BTrace 注解
Target.java — 模拟的线上应用.
import java.util.concurrent.TimeUnit;
public class Target {
public static void main(String[] args) throws InterruptedException {
try {
TimeUnit.SECONDS.sleep(15);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("start main method...");
while(true) {
System.out.println("starting new thread...");
new Thread(() -> {
while (true) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
TimeUnit.SECONDS.sleep(5);
}
}
}
运行 Target.java 后,使用 jps -v 查看该程序 pid 号。
> jps -v
21952 Target -javaagent:D:\program\JetBrains\IntelliJ IDEA017.2.2\lib\idea_rt.jar=55132:D:\program\JetBrains\IntelliJ
IDEA 2017.2.2\bin -Dfile.encoding=UTF-8
使用 btrace 启用 Tracer.java。
## 首先切换到 Tracer.java 所在目录,然后执行
> btrace -v 21952 Tracer.java
- -v 打印出 DEBUG 信息,无 -v 只输出你自己想要输出的信息
可以看到在 Target.java 和 BTrace 的窗口都输出了调试信息。在 Target 中一旦一个新线程被创建,BTrace 便可以打印出相关信息。
继续 — BTrace 注解
假设我们想诊断这样一个 add 方法:
Target.java.
package com.foo.btraceDemo;
import java.lang.management.ManagementFactory;
import java.util.concurrent.TimeUnit;
public class Target {
private static final String pid = ManagementFactory.getRuntimeMXBean().getName().split("@")[0];
public static void main(String[] args) throws InterruptedException {
System.out.println("pid: " + pid);
while(true) {
System.out.println("准备调用 add 方法...");
add(3, 5);
System.out.println("调用 add 方法结束...");
}
}
public static int add(int a, int b) throws InterruptedException {
TimeUnit.SECONDS.sleep(5);
a = a + a;
System.out.println("dododo");
a = a / 2;
return a + b;
}
}
Method Annotations 方法注释
@OnMethod
Tracer.java:func.
@OnMethod(clazz="com.foo.btraceDemo.Target", method="add", location = @Location(Kind.RETURN)) public static void func(@Return int result, @Duration long time, int paramA, int paramB) { println("param:" + paramA + ", " + paramB); println("result:" + result); println("costs(ms):" + time/1000/1000); }
输出结果
param:3, 5 result:8 costs(ms):5020 param:3, 5 result:8 costs(ms):5013
如何通过 clazz/method 指定要诊断的方法
全限定名
clazz="com.foo.btraceDemo.Target", method="add"
正则表达式
clazz = "+java.sql.Statement", method = "/execute($|Update|Query|Batch)/"
接口/父类,注解
clazz = "+java.sql.Statement" // 匹配所有实现该接口或父类的类 clazz = "@org.springframework.stereotype.Controller" // 匹配所有 @Controller 注解的类
构造函数
clazz="java.lang.Throwable",method="<init>", location=@Location(Kind.RETURN) // 匹配任何异常被构造完成准备抛出
静态内部类
clazz="com.foo.bar$YourInnerClass", method="mName")
重载方法区别方法见下文
如何通过 @Location 指定诊断方法的时机
Kind.Entry Kind.Return
Kind.Entry 方法进入时,为默认值。
Kind.Return 方法完成时, 指定此 Kind 可以使用 @Duration 获取方法耗时, @Return 获取方法返回结果
@OnMethod(clazz="com.foo.btraceDemo.Target", method="add", ocation = @Location(Kind.RETURN)) public static void func(@Return int result, @Duration long me, int paramA, int paramB) { println("param:" + paramA + ", " + paramB); println("result:" + result); println("costs(ms):" + time/1000/1000); }
Kind.Error, Kind.Throw和 Kind.Catch
Kind.Error: 异常抛出方法之外
Kind.Throw: 异常被 throw 之处
Kind.Catch: 异常被 catch 之处
@OnMethod(clazz = "java.net.ServerSocket", method = "bind", ocation = @Location(Kind.ERROR)) public static void onBind(Throwable exception, @Duration long uration) // 这种写法待验证
Kind.Call与Kind.Line
Kind.ENTRY (默认值)只关注目标方法(OnMethod 的clazz/method),Kind.CALL 关注目标方法中,调用的其它哪些类哪些方法(Location的class,method)。注意,一般网上的教程,在描述 Kind.ENTRY 和 Kind.CALL 时,会将 CALL 描述为“还关注目标方法中其它方法的调用”,但是经过验证,此处不是“还”的关系,而是过滤的逻辑。也就是说,指定了这里的 clazz 和 method(例如 MethodB),将忽略目标方法(MethodA)的其它逻辑的时间。示例代码如下:
Target.java — add.
public int add(int a, int b) throws InterruptedException { TimeUnit.SECONDS.sleep(5); a = a + a; System.out.println("dododo"); a = a / 2; return a + b; }
Kind.ENTRY.
@OnMethod(clazz = "com.foo.btraceDemo.Target", method = "add", location = @Location(value = Kind.RETURN)) public static void add1(@Duration long time, int a, int b) { print("add1 costs1(ns):" + time); totalTime += time; counter ++; println(", average(ns): " + (totalTime/counter)); } add1 costs1(ns):5053722750, average(ns): 5053722750 add1 costs1(ns):5050729632, average(ns): 5052226191 add1 costs1(ns):5036155712, average(ns): 5046869364
KIND.CALL 匹配 println.
@OnMethod(clazz = "com.foo.btraceDemo.Target", method = "add", location = @Location(value = Kind.CALL, clazz = "java.io.PrintStream", method = "println", where = Where.AFTER)) public static void add2(@Duration long time) { print("add2 costs(ns):" + time); totalTime += time; counter ++; println(", average(ns): " + (totalTime/counter)); } add2 costs(ns):148064, average(ns): 148064 add2 costs(ns):91911, average(ns): 119987 add2 costs(ns):143314, average(ns): 127763
KIND.CALL 匹配 *.
@OnMethod(clazz = "com.foo.btraceDemo.Target", method = "add", location = @Location(value = Kind.CALL, clazz = "/.*/", method = "/.*/", where = Where.AFTER)) public static void add3(@Duration long time) { print("add3 costs(ns):" + time); totalTime += time; counter ++; println(", average(ns): " + (totalTime/counter)); } add3 costs(ns):5166920098, average(ns): 5166920098 add3 costs(ns):81295, average(ns): 2583500696 add3 costs(ns):5160914306, average(ns): 3442638566 add3 costs(ns):68445, average(ns): 2581996036 add3 costs(ns):5075955184, average(ns): 3080787865 add3 costs(ns):67048, average(ns): 2567334396 add3 costs(ns):5060307945, average(ns): 2923473474 add3 costs(ns):115098, average(ns): 2558053677 add3 costs(ns):5276136213, average(ns): 2860062848 add3 costs(ns):93308, average(ns): 2574065894
- 注意这种写法打印的时间,你看出什么规律了吗?
@OnTimer 定时任务,单位 ms, 没有什么好解释的。
@OnTimer(5000) public static void run(){ println(counter); }
@OnEvent
@OnEvent public static void defalutEvent(){ println("default event sent..."); } @OnEvent("speak") public static void namedEvent(){ println("speak something..."); }
输出结果
Please enter your option: 1. exit 2. send an event 3. send a named event 4. flush console output >3 Please enter the event name: >speak speak something... >1 default event sent...
@OnError
Arguments Annotations 参数注释
jstat -J-Djstat.showUnsupported=true -name btrace.com.sun.btrace.samples.ThreadCounter.count <pid>
更多解释请参考 BTrace 本地安装路径下的帮助文档,或者 wiki