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

一灰灰發表於2020-03-17

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

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

前面有兩篇鋪墊博文,在博文《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

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

到這裡本文的 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 的方式,執行上面的程式碼,會發現拋異常,無法正常執行了

手把手教你實現一個方法耗時統計的 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 來修改位元組碼,為每個方法的第一行和最後一行注入耗時統計的程式碼,從而實現方法耗時統計

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

(具體解決方案可以在原始碼中獲取哦,還有配套的測試 case,求支援,求贊,求關注 ❀)

II. 其他

0. 相關

相關博文

相關原始碼

1. 一灰灰 Blogliuyueyi.github.io/hexblog

一灰灰的個人部落格,記錄所有學習和工作中的博文,歡迎大家前去逛逛

2. 宣告

盡信書則不如,已上內容,純屬一家之言,因個人能力有限,難免有疏漏和錯誤之處,如發現 bug 或者有更好的建議,歡迎批評指正,不吝感激

3. 掃描關注

一灰灰 blog

QrCode

相關文章