手把手教你實現一個方法耗時統計的 java agent

1. 基本姿勢點

上面兩節雖然手把手教你實現了一個 hello world 版 agent,然而實際上對 java agent 依然是一臉茫然,所以我們得先補齊一下基礎知識

首先來看 agent 的兩個方法中的參數 Instrumentation,我們先看一下它的接口定義

<code>/**
* 註冊一個Transformer,從此之後的類加載都會被Transformer攔截。
* Transformer可以直接對類的字節碼byte[]進行修改
*/
void addTransformer(ClassFileTransformer transformer);

/**
* 對JVM已經加載的類重新觸發類加載。使用的就是上面註冊的Transformer。
* retransformation可以修改方法體,但是不能變更方法簽名、增加和刪除方法/類的成員屬性
*/
void retransformClasses(Class>... classes) throws UnmodifiableClassException;

/**
* 獲取一個對象的大小
*/
long getObjectSize(Object objectToSize);

/**
* 將一個jar加入到bootstrap classloader的 classpath裡
*/
void appendToBootstrapClassLoaderSearch(JarFile jarfile);

/**
* 獲取當前被JVM加載的所有類對象
*/
Class[] getAllLoadedClasses();/<code>

前面兩個方法比較重要,addTransformer 方法配置之後,後續的類加載都會被 Transformer 攔截。對於已經加載過的類,可以執行 retransformClasses 來重新觸發這個 Transformer 的攔截。類加載的字節碼被修改後,除非再次被 retransform,否則不會恢復。

通過上面的描述,可知

  • 可以通過Transformer修改類
  • 類加載時,會被觸發 Transformer 攔截

2. 實現

我們需要統計方法耗時,所以想到的就是在方法的執行前,記錄一個時間,執行完之後統計一下時間差,即為耗時

直接修改字節碼有點麻煩,因此我們藉助神器javaassist來修改字節碼

實現自定義的ClassFileTransformer,代碼如下

<code>public class CostTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) {
// 這裡我們限制下,只針對目標包下進行耗時統計
if (!className.startsWith("com/git/hui/java/")) {
return classfileBuffer;
}

CtClass cl = null;
try {
ClassPool classPool = ClassPool.getDefault();
cl = classPool.makeClass(new ByteArrayInputStream(classfileBuffer));

for (CtMethod method : cl.getDeclaredMethods()) {
// 所有方法,統計耗時;請注意,需要通過`addLocalVariable`來聲明局部變量
method.addLocalVariable("start", CtClass.longType);

method.insertBefore("start = System.currentTimeMillis();");
String methodName = method.getLongName();
method.insertAfter("System.out.println(\"" + methodName + " cost: \" + (System" +
".currentTimeMillis() - start));");
}

byte[] transformed = cl.toBytecode();
return transformed;
} catch (Exception e) {
e.printStackTrace();
}
return classfileBuffer;
}
}/<code>

然後稍微改一下 agent

<code>/**
* Created by @author yihui in 16:39 20/3/15.
*/
public class SimpleAgent {

/**
* jvm 參數形式啟動,運行此方法
*
* manifest需要配置屬性Premain-Class
*
* @param agentArgs
* @param inst
*/
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("premain");
customLogic(inst);
}

/**
* 動態 attach 方式啟動,運行此方法
*
* manifest需要配置屬性Agent-Class
*
* @param agentArgs
* @param inst
*/
public static void agentmain(String agentArgs, Instrumentation inst) {
System.out.println("agentmain");
customLogic(inst);

}

/**
* 統計方法耗時
*
* @param inst
*/
private static void customLogic(Instrumentation inst) {
inst.addTransformer(new CostTransformer(), true);
}
}/<code>

到此 agent 完畢,打包和上面的過程一樣,接下來進入測試環節

創建一個 DemoClz, 裡面兩個方法

<code>public class DemoClz {

public int print(int i) {
System.out.println("i: " + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return i + 2;
}

public int count(int i) {
System.out.println("cnt: " + i);
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
return i + 1;
}
}/<code>

然後對應的 main 方法如下

<code>public class BaseMain {
public static void main(String[] args) throws InterruptedException {
DemoClz demoClz = new DemoClz();
int cnt = 0;

for (int i = 0; i < 20; i++) {
if (++cnt % 2 == 0) {
i = demoClz.print(i);
} else {
i = demoClz.count(i);
}
}
}
}/<code>

選擇 jvm 參數指定 agent 方式運行(具體操作和上面一樣),輸出如下


手把手教你實現一個方法耗時統計的 java agent


雖然我們的應用程序中並沒有方法的耗時統計,但是最終的輸出卻完美的打印了每個方法的調用耗時,實現了無侵入的耗時統計功能

到這裡本文的 java agent 的掃盲 + 實戰(開發一個方法耗時統計)都已經完成了,是否就宣告著可以小結了,並不是,下面介紹一下在實現上面的 demo 過程中遇到的一個問題

3. Exception in thread "main" java.lang.VerifyError: Expecting a stack map frame

在演示方法耗時的 agent 的示例中,並沒有藉助最開始的測試用例,而是新建了一個DemoClz來做的,那麼為什麼這樣選擇呢,如果直接用第二節的測試用例會怎樣呢?

<code>public class BaseMain {
public int print(int i) {
System.out.println("i: " + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return i + 2;
}

public void run() {
int i = 1;
while (true) {
i = print(i);
}
}

public static void main(String[] args) {
BaseMain main = new BaseMain();
main.run();
}/<code>

依然通過 jvm 參數指定 agent 的方式,運行上面的代碼,會發現拋異常,無法正常運行了


手把手教你實現一個方法耗時統計的 java agent


指出了在 run 方法這裡,存在字節碼的錯誤,我們統計耗時的 Agent,主要就是在方法開始前和結束後各自新增了一行代碼,我們直接補充在 run 方法中,則相當於下面的代碼


手把手教你實現一個方法耗時統計的 java agent


上面的提示很明顯的告訴了,最後一行語句永遠不可能達到,編譯就存在異常了;那麼問題來了,作為一個 java agent 的提供者,我哪知道使用者有沒有寫這種死循環的方法,如果應用中有這麼個死循環的任務存在,把我的 agent 一掛載上去,導致應用都起不來,這個鍋算誰的????

下面提供解決方案,也很簡單,在 jvm 參數中,添加一個-noverify (請注意不同的 jdk 版本,參數可能不一樣,我的本地是 jdk8,用這個參數;如果是 jdk7 可以試一下-XX:-UseSplitVerifier)

在 IDEA 開發環境下,如下配置即可


手把手教你實現一個方法耗時統計的 java agent


再次運行,正常了


手把手教你實現一個方法耗時統計的 java agent


4. 小結

本篇為實戰項目,首先明確方法參數Instrumentation它的接口定義,通過它來實現 java 字節碼的修改

我們通過實現自定義的ClassFileTransformer,藉助 javassist 來修改字節碼,為每個方法的第一行和最後一行注入耗時統計的代碼,從而實現方法耗時統計

最後留一個小問題,上面的實現中,當方法內部拋出異常時,我們注入的最後一行統計耗時會不會如期輸出,如果不會,應該怎麼修改,歡迎各位大佬留言指出解決方案


作者:一灰灰
鏈接:https://juejin.im/post/5e7040536fb9a07c8334f965


分享到:


相關文章: