Java
Java虚拟机

Java虚拟机14 - JVM工具之Btrace

简介:Java开发工具包中提供了众多的虚拟机工具可供开发者使用。

1. BTrace配置及用法

注:原文链接:http://calvin1978.blogcn.com/articles/btrace1.html

BTrace是可以作为VisualVM插件安装,本身也是可以独立运行的程序。它的作用是在不停止目标程序运行的前提下,通过HotSpot虚拟机的HotSwap技术动态加入原本并不存在的调试代码。这项功能对实际生产中的程序很有意义:经常遇到程序出现问题,但排查错误的一些必要信息,譬如方法参数、返回值等,在开发时并没有打印到日志之中,以至于不得不停掉服务,通过调试增量来加入日志代码以解决问题,代价很大。BTrace应运而生,可以动态地跟踪Java运行程序,将跟踪字节码注入到运行类中,对运行代码侵入较小,对性能上的影响可以忽略不计。

在VisualVM中安装了BTrace插件后,在应用程序面板中右键点击要调试的程序,会出现“Trace Application…”菜单,点击将进入BTrace面板。这个面板里面看起来就像一个简单的Java程序开发环境,里面还有一小段Java代码:

1.BTrace初始页面.png

同时,Btrace也可以在命令行下执行。首先需要去官网下载BTrace,配置环境变量以便在任何路径下能执行btrace命令。btrace命令的格式如下:

注1:项目主页:https://github.com/btraceio/btrace

注2:用户指南:https://zcfy.cc/original/btrace-wiki-userguide-mdash-project-kenai-952.html?t=unclaimed

注3:项目示例:https://github.com/btraceio/btrace/tree/master/samples

  • $ > btrace [-p <port>] [-cp <classpath>] <pid> <btrace-script>
  • port:指定BTrace Agent的服务端监听端口号,用来监听Clients,默认为2020,可选。
  • classpath:用来指定类加载路径,比如你的BTrace代码里用到了第三方jar包。
  • pid:表示进程号,可通过jps命令获取。
  • btrace-script:即为BTrace脚本。

如下测试命令:

  • $ > ./btrace -p 2020 -cp /home/LennonChin/Software/tomcat7/lib/servlet-api.jar (jps | grep Bootstrap | awk '{print $1}') /home/LennonChin/Test/BtraceTestScript.java

可以通过-o参数将结果输出到文件:

  • $ > ./btrace -o trace.log $pid BtraceTestScript.java

该日志文件会生成在应用的启动目录,而不是BTrace命令的启动目录。其次,执行过一次-o之后,再执行BTrace命令不加-o也不会再输出回终端,直到应用重启为止。所以一般重定向输出更为方便:

  • $ > ./btrace $pid BtraceTestScript.java > trace.log

在提交给线上执行时,可以用btracec命令对编写的脚本预编译一下确保脚本的正确性:

  • $ > ./btracec BtraceTestScript.java

2. Btrace脚本入门

Btrace官方提供了一个UserGuide的例子:

  • import com.sun.btrace.annotations.*;
  • import static com.sun.btrace.BTraceUtils.*;
  • @BTrace
  • public class HelloWorld {
  • @OnMethod(clazz = "java.lang.Thread", method = "start")
  • public static void onThreadStart() {
  • println("thread start!");
  • }
  • }

该脚本可以用于监控Thread线程类的start()方法被调用。

BTrace可以用于不限于下面的场景:

  1. 服务性能变慢,分析每个方法的耗时情况;
  2. 当在Map中插入大量数据,分析其扩容情况;
  3. 分析哪个方法调用了System.gc(),调用栈情况;
  4. 执行某个方法抛出异常时,分析运行时参数;

由于Btrace会把脚本逻辑直接侵入到运行的代码中,所以在使用上做很多限制:

  1. 不能创建对象;
  2. 不能使用数组;
  3. 不能抛出或捕获异常;
  4. 不能使用循环;
  5. 不能使用synchronized关键字;
  6. 属性和方法必须使用static修饰;
  7. 在以前的例子里,甚至还不能字符串相加,必须用strcat函数。如println(strcat("方法参数A:" + str(a)));

根据官方声明,不恰当的使用BTrace可能导致JVM崩溃,如在BTrace脚本使用错误的class文件,所以在上生产环境之前,务必在本地充分的验证脚本的正确性。

注:可以用-u运行在Unsafe Mode来规避限制,但不推荐。

3. BTrace脚本具体使用

3.1. 拦截方法定义

3.2. 精准定位

官方给出的User Guide中的HelloWorld的例子即是精确定义要监控的类与方法。

3.3. 正则表达式定位

可以用表达式,批量定义需要监控的类与方法。正则表达式需要写在两个/中间。下例监控javax.swing下的所有类的所有方法:

  • @OnMethod(clazz = "/javax\\.swing\\..*/", method = "/.*/")
  • public static void swingMethods( @ProbeClassName String probeClass, @ProbeMethodName String probeMethod) {
  • print("entered " + probeClass + "." + probeMethod);
  • }

通过在拦截函数的定义里注入@ProbeClassName String probeClass@ProbeMethodName String probeMethod参数,告诉脚本实际匹配到的类和方法名。

另一个例子,监控Statement的executeUpdate()executeQuery()executeBatch()三个方法,可以查阅官方提供的例子JdbcQueries.java

3.4. 按接口、父类、Annotation定位

比如我想匹配所有的Filter类,在接口或基类的名称前面,加个+就行:

  • @OnMethod(clazz = "+com.vip.demo.Filter", method = "doFilter")

也可以按类或方法上的annotaiton匹配,前面加上@就行:

  • @OnMethod(clazz = "@javax.jws.WebService", method = "@javax.jws.WebMethod")

3.5. 其他

  1. 构造函数的名字是<init>
  • @OnMethod(clazz = "java.net.ServerSocket", method = "<init>")
  1. 静态内部类的写法,是在类与内部类之间加上$
  • @OnMethod(clazz = "com.coderap.tools.visualvm.BTraceTest$TestInnerClass", method = "test")
  1. 如果有多个同名的函数,想区分开来,可以在拦截函数上定义不同的参数列表。

4. 拦截时机

可以为同一个函数的不同的Location,分别定义多个拦截函数。

4.1. Kind.ENTRY与Kind.Return

  • @OnMethod(clazz = "java.net.ServerSocket", method="bind")

不写Location,默认就是刚进入函数的时候Kind.ENTRY

如果你想获得函数的返回结果或执行时间,则必须把切入点定在返回Kind.RETURN时:

  • @OnMethod(clazz = "java.net.ServerSocket", method = "getLocalPort", location = @Location(Kind.RETURN))
  • public static void onGetPort(@Return int port, @Duration long duration) {
  • ...
  • }

duration的单位是纳秒,要除以1,000,000才是毫秒。

4.2. Kind.ERROR, Kind.THROW和 Kind.CATCH

异常抛出(Throw),异常被捕获(Catch),异常没被捕获被抛出函数之外(Error),主要用于对某些异常情况的跟踪。在拦截函数的参数定义里注入一个Throwable的参数,代表异常。

  • @OnMethod(clazz = "java.net.ServerSocket", method = "bind", location = @Location(Kind.ERROR))
  • public static void onBind(Throwable exception, @Duration long duration) {
  • ...
  • }

4.3. Kind.CALL与Kind.LINE

下例定义监控bind()函数里调用的所有其他函数:

  • @OnMethod(clazz = "java.net.ServerSocket", method = "bind", location = @Location(value = Kind.CALL, clazz = "/.*/", method = "/.*/", where = Where.AFTER))
  • public static void onBind(@Self Object self, @TargetInstance Object instance, @TargetMethodOrField String method, @Duration long duration) {
  • ...
  • }

所调用的类及方法名所注入到@TargetInstance@TargetMethodOrField中。

​静态函数中,instance的值为空。如果想获得执行时间,必须把Where定义成AFTER。

注意这里,一定不要像下面这样大范围的匹配,否则这性能是神仙也没法救了:

  • @OnMethod(clazz = "/javax\\.swing\\..*/", method = "/.*/", location = @Location(value = Kind.CALL, clazz = "/.*/", method = "/.*/"))

下例监控代码是否到达了Socket类的第363行。

  • @OnMethod(clazz = "java.net.ServerSocket", location = @Location(value = Kind.LINE, line = 363))
  • public static void onBind4() {
  • println("socket bind reach line:363");
  • }

line还可以为-1,然后每行都会打印出来,加参数int line获得的当前行数。此时会显示函数里完整的执行路径,但肯定又非常慢。

5. 打印this、参数与返回值

5.1. 定义注入

  • import com.sun.btrace.AnyType;
  • @OnMethod(clazz = "java.io.File", method = "createTempFile", location = @Location(value = Kind.RETURN))
  • public static void o(@Self Object self, String prefix, String suffix, @Return AnyType result) {
  • ...
  • }

如果想打印它们,首先按顺序定义用@Self注释的this, 完整的参数列表,以及用@Return注释的返回值。需要打印哪个就定义哪个,不需要的就不要定义。但定义一定要按顺序,比如参数列表不能跑到返回值的后面。

  • Self:如果是静态函数,self为空。前面提到,如果上述使用了非JDK的类,命令行里要指定classpath。不过,如前所述,因为BTrace里不允许调用类的方法,所以定义具体类很多时候也没意思,所以self定义为Object就够了。
  • 参数:参数数列表要么不要定义,要定义就要定义完整,否则BTrace无法处理不同参数的同名函数。如果有些参数你实在不想引入非JDK类,又不会造成同名函数不可区分,可以用AnyType来定义(不能用Object)。如果拦截点用正则表达式中匹配了多个函数,函数之间的参数个数不一样,你又还是想把参数打印出来时,可以用AnyType[] args来定义。但不知道是不是当前版本的bug,AnyType[] args不能和location=Kind.RETURN同用,否则会进入一种奇怪的静默状态,只要有一个函数定义错了,整个Btrace就什么都打印不出来。
  • 结果:同理,结果也可以用AnyType来定义,特别是用正则表达式匹配多个函数的时候,连void都可以表示。

5.2. 打印

再次强调,为了保证性能不受影响,Btrace不允许调用任何实例方法。比如不能调用Getter方法(怕在Getter里有复杂的计算),只会通过直接反射来读取属性名。又比如,除了JDK类,其他类toString()时只会打印其类名及System.IdentityHashCode。println()printArray()都按上面的规律进行,所以只能打打基本类型。

如果想打印一个Object的属性,用printFields()来反射。如果只想反射某个属性,参照下面打印Port属性的写法。从性能考虑,应把field用静态变量缓存起来。注意JDK类与非JDK类的区别:

  • import java.lang.reflect.Field;
  • // JDK的类这样写就行
  • private static Field fdFiled = field("java.io,FileInputStream", "fd");
  • // 非JDK的类,要给出ClassLoader,否则ClassNotFound
  • private static Field portField = field(classForName("com.coderap.tools.visualvm.BTraceTest", contextClassLoader()), "port");
  • public static void onChannelRead(@Self Object self) {
  • println("port:" + getInt(portField, self));
  • }

5.3. TLS,拦截函数间的通信机制

如果要多个拦截函数之间要通信,可以使用@TLS定义ThreadLocal的变量来共享:

  • @TLS
  • private static int port = -1;
  • @OnMethod(clazz = "java.net.ServerSocket", method = "<init>")
  • public static void onServerSocket(int p){
  • port = p;
  • }
  • @OnMethod(clazz = "java.net.ServerSocket", method = "bind")
  • public static void onBind(){
  • println("server socket at " + port);
  • }

6. 典型场景

6.1. 打印慢调用

下例打印所有用时超过1毫秒的Filter:

  • @OnMethod(clazz = "+com.coderap.tools.visualvm.Filter", method = "doFilter", location = @Location(Kind.RETURN))
  • public static void onDoFilter2(@ProbeClassName String pcn, @Duration long duration) {
  • if (duration > 1000000) {
  • println(pcn + ",duration:" + (duration / 100000));
  • }
  • }

最好能抽取了打印耗时的函数,减少代码重复度。定位到某一个Filter慢了之后,可以直接用Location(Kind.CALL),进一步找出它里面的哪一步慢了。

6.2. 谁调用了某个方法

比如,谁调用了System.gc()方法:

  • @OnMethod(clazz = "java.lang.System", method = "gc")
  • public static void onSystemGC() {
  • println("entered System.gc()");
  • jstack();
  • }

6.3. 捕捉异常,或进入了某个特定代码行时,this对象及参数的值

按之前的提示,自己组合一下即可。

6.4. 打印函数的调用/慢调用的统计信息

如果你已经看到了这里,那基本也不用我再啰嗦了,自己看Samples的Histogram.java, HistoOnEvent.java,可以用AtomicInteger构造计数器,然后定时@OnTimer,或根据事件@OnEvent输出结果(Ctrl + C后选择发送事件)。