前面有兩篇鋪墊博文,在博文《200303-如何優雅的在 java 中統計程式碼塊耗時》,其最後提到了根據利用 java agent 來統計方法耗時
博文《200316-IDEA + maven 零基礎構建 java agent 專案》中則詳細描述了搭建一個 java agent 開發測試專案的全過程
本篇博文將進入 java agent 的實戰,手把手教你如何是實現一個統計方法耗時的 java agent
1. 基本姿勢點
上面兩節雖然手把手教你實現了一個 hello world 版 agent,然而實際上對 java agent 依然是一臉茫然,所以我們得先補齊一下基礎知識
首先來看 agent 的兩個方法中的引數 Instrumentation
,我們先看一下它的介面定義
/**
* 註冊一個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();
複製程式碼
前面兩個方法比較重要,addTransformer 方法配置之後,後續的類載入都會被 Transformer 攔截。對於已經載入過的類,可以執行 retransformClasses 來重新觸發這個 Transformer 的攔截。類載入的位元組碼被修改後,除非再次被 retransform,否則不會恢復。
通過上面的描述,可知
- 可以通過
Transformer
修改類 - 類載入時,會被觸發 Transformer 攔截
2. 實現
我們需要統計方法耗時,所以想到的就是在方法的執行前,記錄一個時間,執行完之後統計一下時間差,即為耗時
直接修改位元組碼有點麻煩,因此我們藉助神器javaassist
來修改位元組碼
實現自定義的ClassFileTransformer
,程式碼如下
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;
}
}
複製程式碼
然後稍微改一下 agent
/**
* 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);
}
}
複製程式碼
到此 agent 完畢,打包和上面的過程一樣,接下來進入測試環節
建立一個 DemoClz, 裡面兩個方法
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;
}
}
複製程式碼
然後對應的 main 方法如下
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);
}
}
}
}
複製程式碼
選擇 jvm 引數指定 agent 方式執行(具體操作和上面一樣),輸出如下
雖然我們的應用程式中並沒有方法的耗時統計,但是最終的輸出卻完美的列印了每個方法的呼叫耗時,實現了無侵入的耗時統計功能
到這裡本文的 java agent 的掃盲 + 實戰(開發一個方法耗時統計)都已經完成了,是否就宣告著可以小結了,並不是,下面介紹一下在實現上面的 demo 過程中遇到的一個問題
3. Exception in thread "main" java.lang.VerifyError: Expecting a stack map frame
在演示方法耗時的 agent 的示例中,並沒有藉助最開始的測試用例,而是新建了一個DemoClz
來做的,那麼為什麼這樣選擇呢,如果直接用第二節的測試用例會怎樣呢?
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();
}
複製程式碼
依然通過 jvm 引數指定 agent 的方式,執行上面的程式碼,會發現拋異常,無法正常執行了
指出了在 run 方法這裡,存在位元組碼的錯誤,我們統計耗時的 Agent,主要就是在方法開始前和結束後各自新增了一行程式碼,我們直接補充在 run 方法中,則相當於下面的程式碼
上面的提示很明顯的告訴了,最後一行語句永遠不可能達到,編譯就存在異常了;那麼問題來了,作為一個 java agent 的提供者,我哪知道使用者有沒有寫這種死迴圈的方法,如果應用中有這麼個死迴圈的任務存在,把我的 agent 一掛載上去,導致應用都起不來,這個鍋算誰的????
下面提供解決方案,也很簡單,在 jvm 引數中,新增一個-noverify
(請注意不同的 jdk 版本,引數可能不一樣,我的本地是 jdk8,用這個引數;如果是 jdk7 可以試一下-XX:-UseSplitVerifier
)
在 IDEA 開發環境下,如下配置即可
再次執行,正常了
4. 小結
本篇為實戰專案,首先明確方法引數Instrumentation
它的介面定義,通過它來實現 java 位元組碼的修改
我們通過實現自定義的ClassFileTransformer
,藉助 javassist 來修改位元組碼,為每個方法的第一行和最後一行注入耗時統計的程式碼,從而實現方法耗時統計
最後留一個小問題,上面的實現中,當方法內部丟擲異常時,我們注入的最後一行統計耗時會不會如期輸出,如果不會,應該怎麼修改,歡迎各位大佬留言指出解決方案
(具體解決方案可以在原始碼中獲取哦,還有配套的測試 case,求支援,求贊,求關注 ❀)
II. 其他
0. 相關
相關博文
相關原始碼
1. 一灰灰 Blog: liuyueyi.github.io/hexblog
一灰灰的個人部落格,記錄所有學習和工作中的博文,歡迎大家前去逛逛
2. 宣告
盡信書則不如,已上內容,純屬一家之言,因個人能力有限,難免有疏漏和錯誤之處,如發現 bug 或者有更好的建議,歡迎批評指正,不吝感激
- 微博地址: 小灰灰 Blog
- QQ: 一灰灰/3302797840
3. 掃描關注
一灰灰 blog