开发工具系列(一):Btrace——线上Debug工具

Btrace

Btrace用于调试正在运行的系统,并且在调试时不会暂停系统。特别适用于跟踪线上问题。你可以实时监控一个系统中任何一个方法的调用,你可以知道这些方法的参数、返回值是什么,还可以知道方法调用消耗了多少时间。

Btrace不需要安装,只要下载一个包,解压即可。

Btrace用法为bin/btrace <pid> <trace-script>。其中pid是正在运行的java进程,trace-script是跟踪脚本,它其实就是一段java代码。

Hello World

首先我们模拟一个正在运行的程序,它仅有一个循环。

package com.caipeichao;
 
public class NullApp {
 
    public static void main(String[] argv) {
        new NullApp().run();
    }
 
    public void run() {
        for (int i = 0; i < 100000; i++) {
            sleep(1000);
            new MyObj().life(i);
        }
    }
 
    private static class MyObj {
 
        public void life(int n) {
            System.out.println(n);
        }
    }
 
    private void sleep(int n) {
        try {
            Thread.sleep(n);
        } catch (InterruptedException e) {
        }
    }
}

然后开启这个程序: java com.caipeichao.NullApp

通过jps命令得到这个程序的PID,这里为13348。

> jps
3034 RemoteMavenServer
2902 Main
15147 Jps
13348 NullApp

准备工作做完了,现在编写最重要的跟踪脚本。

import static com.sun.btrace.BTraceUtils.*;
import com.sun.btrace.annotations.*;
 
@BTrace
public class HelloBtrace {
  // 当com.caipeichao.NullApp.sleep方法返回时,执行该方法 
  @OnMethod(clazz="com.caipeichao.NullApp",
    method="sleep",
    location=@Location(Kind.RETURN))
  public static void onSleep() {
    println("Hello world");
  }
}

运行btrace,得到如下输出。

> btrace 13348 HelloBtrace.java
Hello world
Hello world
Hello world
Hello world
Hello world
Hello world

常用注解

名称 作用域 作用
@BTrace 声明跟踪脚本
@OnMethod(clazz,method,location) 方法 当指定方法被调用时
@OnMethod(method="<init>") 方法 当构造函数被调用时
@OnMethod(clazz="/java\\.io\\..*Input/")) 方法 方法名称正则匹配
@Location(kind) @OnMethod 指定监控方法调用前还是调用后
@Location(value=Kind.NEWARRAY, clazz="char") @OnMethod 监控新增数组
@Self 参数 表示被监控的对象
@ProbeMethodName 参数 被监控的方法名称
@ProbeClassName 参数 被监控的类名
@OnTimer(interval) 方法 定时调用某个方法
@OnLowMemory(pool,threshold) 方法 当内存不足时
@OnExit 方法 当程序退出时
@OnProbe(namespace="java.net.socket",name="bind") 方法 监控socket中的bind方法

常用方法

方法 作用
println 在本地控制台输出一行
print 在本地控制台输出
printArray 在本地控制台输出数组
jstack 打印远程方法的调用调用栈
jstackAll 输出所有线程的调用栈
exit 退出跟踪脚本
Strings.strcat 连接字符串
Reflactive.name 获取类名
Threads.name 线程名
Threads.currentThread 当前线程
deadlocks 打出死锁线程
sizeof 获取对象的大小,比如List对象就返回List.size()
Sys.Env.property 获取系统变量

原理

BTrace利用了java.lang.instrument包实现代码注入。首先通过VirtualMachine.attach(pid)连接远程JVM,然后通过VirtualMachine.loadAgent("*.jar")加载一个btrace的jar包。这个jar包最重要的代码如下。

public static void premain(String args, Instrumentation inst) {
  main(args, inst);
}
 
public static void agentmain(String args, Instrumentation inst) {
  main(args, inst);
}
 
// 将btrace的jar包添加到ClassLoader搜索目录 
private static synchronized void main(final String args, final Instrumentation inst) {
  ...
  inst.appendToBootstrapClassLoaderSearch(new JarFile(new File(path)));
  ...
  inst.appendToSystemClassLoaderSearch(new JarFile(new File(path)));
  ...
  startServer();
}
 
// 开启服务 
private static void startServer() {
  ...
  while (true) {
    try {
      ...
      handleNewClient(client);
    } catch (RuntimeException re) {
      if (isDebug()) debugPrint(re);
    } catch (IOException ioexp) {
      if (isDebug()) debugPrint(ioexp);
    }
  }
}
 
// 修改内存中的类定义 
private static void handleNewClient(final Client client) {
  ...
  inst.addTransformer(client, true);
  ...
  inst.retransformClasses(classes);
}
 
// 用ASM动态生成字节码 
abstract class Client implements ClassFileTransformerCommandListener {
  static {
    ClassFilter.class.getClass();
    ClassReader.class.getClass();
    ClassWriter.class.getClass();
    ...
  }
 
  private byte[] instrument(Class clazzString cnamebyte[] target) {
    byte[] instrumentedCode;
    try {
    ClassWriter writer = InstrumentUtils.newClassWriter(target);
    ClassReader reader = new ClassReader(target);
    Instrumentor i = new Instrumentor(clazz, className,  btraceCode, onMethods, writer);
    ...
  }
}

一句话总结,btrace利用instrument工具修改JVM内存中的类字节码,达到注入代码的目的。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章