開發工具系列(一):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內存中的類字節碼,達到注入代碼的目的。


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