【轉】動態字節碼技術跟蹤Java程序


 Whats is Java Agent?   .. java.lang.instrument.Instrumentation

 

之前有寫 基於AOP的日志調試 討論一種跟蹤Java程序的方法, 但不是很完美.后來發現了 Btrace , 由於它借助動態字節碼注入技術 , 實現優雅且功能強大.
只不過, 用起來總是磕磕絆絆的, 時常為了跟蹤某個問題, 卻花了大把的時間調試Btrace的腳本. 為此, 我嘗試將幾種跟蹤模式固化成腳本模板, 待用的時候去調整一下正則表達式之類的.
跟蹤過程往往是假設與驗證的螺旋迭代過程, 反復的用BTrace跟蹤目標進程, 總有那么幾次莫名其妙的不可用, 最后不得不重啟目標進程. 若真是線上不能停的服務, 我想這種方式還是不靠譜啊.
為此, 據決定自己的搞個用起來簡單, 又能良好支持反復跟蹤而不用重啟目標進程的工具.

AOP

AOP是Btrace, jip1等眾多監測工具的核心思想, 用一段代碼最容易說明:

?
1
2
3
4
5
public void say(String words){
   Trace.enter();
   System.out.println(words);
   Trace.exit();
}

如上, Trace.enter() 和 Trace.exit() 將say(words)內的代碼環抱起來, 對方法進出的進行切面的處理, 便可獲取運行時的上下文, 如:

  • 調用棧
  • 當前線程
  • 時間消耗
  • 參數與返回值
  • 當前實例狀態

實現的選擇

實現切面的方式, 我知道的有以下幾種:

代理(裝飾器)模式

設計模式中裝飾器模式和代理模式, 盡管解決的問題域不同, 代碼實現是非常相似, 均可以實現切面處理, 這里視為等價. 依舊用代碼說明:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
interface Person {
   void say(String words);
}
 
class Officer implements Person {
   public void say(String words) { lie(words); }
   private void lie(String words) {...}
}
 
class Proxy implements Person {
   private final Officer officer;
   public Proxy(Officer officer) { this .officer = officer; }
   public void say(String words) {
     enter();
     officer.say(words);
     exit();
   }
   private void enter() { ... }
   private void exit() { ... }
}
 
Person p = new Proxy( new Officer());

很明顯, 上述enter() 和exit()是實現切面的地方, 通過獲取Officer的Proxy實例, 便可對Officer實例的行為進行跟蹤. 這種方式實現起來最簡單, 也最直接.

Java Proxy

Java Proxy是JDK內置的代理API, 借助反射機制實現. 用它來是完成切面則會是:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class ProxyInvocationHandler implements InvocationHandler {
   private final Object target;
   public ProxyInvocationHandler(Object target) { this .target = target;}
   public Object handle(Object proxy, Method method, Object[] args) {
     enter();
     method.invoke(target, args);
     exit();
   }
   private void enter() { ... }
   private void exit() { ... }
}
ClassLoader loader = ...
Class<?>[]  interfaces = {Person. class };
Person p = (Person)Proxy.newInstance(loader, interfaces, new ProxyInvocationHandler( new Officer()));

相比較上一中方法, 這種不太易讀, 但更為通用, 對具體實現依賴很少.

AspectJ

AspectJ是基於字節碼操作(運行時利用ASM庫)的AOP實現, 相比較Java proxy, 它會顯得對調用更”透明”, 編寫更簡明(類似DSL), 性能更好. 如下代碼:

?
1
2
3
pointcut say(): execute(* say(..))
before(): say() { ... }
after() : say() { ... }

Aspectj實現切面的時機有兩種: 靜態編譯和類加載期編織(load-time weaving). 並且它對IDE的支持很豐富.

CGlib

與AspectJ一樣CGlib也是操作字節碼來實現AOP的, 使用上與Java Proxy非常相似, 只是不像Java Proxy對接口有依賴, 我們熟知的Spring, Guice之類的IoC容器實現AOP都是使用它來完成的.

?
1
2
3
4
5
6
7
8
9
10
11
12
13
class Callback implements MethodInterceptor {
   public Object intercept(Object obj, java.lang.reflect.Method method, Object[] args, MethodProxy proxy) throws Throwable {
     enter();
     proxy.invokeSuper(obj, args);
     exit();
   }
   private void enter() { ... }
   private void exit() { ... }
}
Enhancer e = new Enhancer();
e.setSuperclass(Officer. class );
e.setCallback( new Callback());
Person p = e.create();

字節碼操縱

上面四種方法各有適用的場景, 但唯獨對運行着的Java進程進行動態的跟蹤支持不了, 當然也許是我了解的不夠深入, 若有基於上述方案的辦法還請不吝賜教.

還是回到Btrace的思路上來, 在理解了它借助java.lang.Instrumentation進行字節碼注入的實現原理后, 實現動態變化跟蹤方式或目標應該沒有問題.

借下來的問題, 如何操作(注入)字節碼實現切面的處理. 可喜的是, “構建自己的監測工具”一文給我提供了一個很好的切入點. 在此基礎上, 經過一些對ASM的深入研究, 可以實現:

  • 方法調用進入時, 獲取當前實例(this) 和 參數值列表;
  • 方法調用出去時, 獲取返回值;
  • 方法異常拋出時, 觸發回調並獲取異常實例.

其切面實現的核心代碼如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
private static class ProbeMethodAdapter extends AdviceAdapter {
 
     protected ProbeMethodAdapter(MethodVisitor mv, int access, String name, String desc, String className) {
       super (mv, access, name, desc);
       start = new Label();
       end = new Label();
       methodName = name;
       this .className = className;
     }
 
     @Override
     public void visitMaxs( int maxStack, int maxLocals) {
       mark(end);
       catchException(start, end, Type.getType(Throwable. class ));
       dup();
       push(className);
       push(methodName);
       push(methodDesc);
       loadThis();
       invokeStatic(Probe.TYPE, Probe.EXIT);
       visitInsn(ATHROW);
       super .visitMaxs(maxStack, maxLocals);
     }
 
     @Override
     protected void onMethodEnter() {
       push(className);
       push(methodName);
       push(methodDesc);
       loadThis();
       loadArgArray();
       invokeStatic(Probe.TYPE, Probe.ENTRY);
       mark(start);
     }
 
     @Override
     protected void onMethodExit( int opcode) {
       if (opcode == ATHROW) return ; // do nothing, @see visitMax
       prepareResultBy(opcode);
       push(className);
       push(methodName);
       push(methodDesc);
       loadThis();
       invokeStatic(Probe.TYPE, Probe.EXIT);
     }
 
     private void prepareResultBy( int opcode) {
       if (opcode == RETURN) { // void
         push((Type) null );
       } else if (opcode == ARETURN) { // object
         dup();
       } else {
         if (opcode == LRETURN || opcode == DRETURN) { // long or double
           dup2();
         } else {
           dup();
         }
         box(Type.getReturnType(methodDesc));
       }
     }
 
     private final String className;
     private final String methodName;
     private final Label start;
     private final Label end;
}

更多參考請見這里的 Demo , 它是javaagent, 在伴隨宿主進程啟動后, 提供MBean可用jconsole進行動態跟蹤的管理.

后續的方向

  1. 提供基於Web的遠程交互界面;
  2. 提供基於Shell的本地命令行接口;
  3. 提供Profile統計和趨勢輸出;
  4. 提供跟蹤日志定位與分析.

參考

  1. The Java Interactive Profiler
  2. Proxy Javadoc
  3. Aspectj
  4. CGlib
  5. BTrace User’s Guide
  6. java動態跟蹤分析工具BTrace實現原理
  7. 構建自己的監測工具
  8. ASM Guide
  9. 常用 Java Profiling 工具的分析與比較
  10. AOP@Work: Performance monitoring with AspectJ
  11. The JavaTM Virtual Machine Specification
  12. 來自rednaxelafx的JVM分享, 他的 Blog
  13. BCEL


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM