深入字節碼 -- 計算方法執行時間


什么是字節碼

java程序通過javac編譯之后生成文件.class就是字節碼集合,正是有這樣一種中間碼(字節碼),使得scala/groovy/clojure等函數語言只用實現一個編譯器即可運行在JVM上。
看看一段簡單代碼。

public long getExclusiveTime() {
    long startTime = System.currentTimeMillis();
    System.out.printf("exclusive code");
    long endTime = System.currentTimeMillis();
    return endTime - startTime;
}
public class com.blueware.agent.StartAgent {

編譯后通過命令(javap -c com.blueware.agent.StartAgent)查看,具體含義請參考oracle

public com.blueware.agent.StartAgent();
    Code:
       0: aload_0
       1: invokespecial #1  // Method java/lang/Object."<init>":()V
       4: return

  public long getExclusiveTime();
    Code:
       0: invokestatic  #2                  // Method java/lang/System.currentTimeMillis:()J
       3: lstore_1
       4: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
       7: ldc           #4                  // String exclusive code
       9: iconst_0
      10: anewarray     #5                  // class java/lang/Object
      13: invokevirtual #6                  // Method java/io/PrintStream.printf:(Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream;
      16: pop
      17: invokestatic  #2                  // Method java/lang/System.currentTimeMillis:()J
      20: lstore_3
      21: lload_3
      22: lload_1
      23: lsub
      24: lreturn
}

為什么要學習字節碼

  • 能了解技術背后的原理,更容易寫出高質量代碼;
  • 字節碼設計非常優秀,發展十幾年只僅僅刪除和增加幾個指令,學懂之后長期受益高,如果懂字節碼再學習scala/groovy/clojure會容易很多;
  • 開發框架、監控系統、中間件、語言字節碼技術都是必殺技;

字節碼框架(ASM/Javassist)

操作字節碼框架有很多,具體可以參考博文,下面對比ASM/Javassist

選項 優點 缺點
ASM 速度快、代碼量小、功能強大 要寫字節碼、學習曲線高
Javassist 學習簡單,不用寫字節碼 ASM慢,功能少

Java Instrumentation介紹

指的是可以用獨立於應用程序之外的代理(agent)程序,agent程序通過增強字節碼動態修改或者新增類,利用這樣特性可以設計出更通用的監控、框架、中間件程序,在JVM啟動參數加–javaagent:agent_jar_path/agent.jar即可運行(在JDK5及其后續版本才可以),更多關於Instrumentation知識請參考博文

計算方法執行時間方式

  • 直接在代碼開始和結束出打印當前時間,相減即可得到;
  • 實現一個動態代理,或者借助Spring/AspectJ等框架;
  • 上面兩種實現方式都需要修改代碼或者配置文件,下面我要介紹方式不僅不需要修改代碼,而且效率高;

具體實現方式

1.StartAgent類必須提供premain方法,代碼如下:

public class StartAgent {
    //代理程序入口函數
    public static void premain(String args, Instrumentation inst) {
        System.out.println("agent begin");
        //添加字節碼轉換器
        inst.addTransformer(new PrintTimeTransformer());
        System.out.println("agent end");
    }
}

2.PrintTimeTransformer實現一個轉換器,代碼如下:

//字節碼轉化器類
public class PrintTimeTransformer implements ClassFileTransformer {

    //實現字節碼轉化接口,一個小技巧建議實現接口方法時寫@Override,方便重構
    //loader:定義要轉換的類加載器,如果是引導加載器,則為 null(在這個小demo暫時還用不到)
    //className:完全限定類內部形式的類名稱和中定義的接口名稱,例如"java.lang.instrument.ClassFileTransformer"
    //classBeingRedefined:如果是被重定義或重轉換觸發,則為重定義或重轉換的類;如果是類加載,則為 null
    //protectionDomain:要定義或重定義的類的保護域
    //classfileBuffer:類文件格式的輸入字節緩沖區(不得修改)
    //一個格式良好的類文件緩沖區(轉換的結果),如果未執行轉換,則返回 null。
    @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer)
            throws IllegalClassFormatException {
        //簡化測試demo,直接寫待修改的類(com/blueware/agent/TestTime)
        if (className != null && className.equals("com/blueware/agent/TestTime")) {
            //讀取類的字節碼流
            ClassReader reader = new ClassReader(classfileBuffer);
            //創建操作字節流值對象,ClassWriter.COMPUTE_MAXS:表示自動計算棧大小
            ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_MAXS);
            //接受一個ClassVisitor子類進行字節碼修改
            reader.accept(new TimeClassVisitor(writer, className), 8);
            //返回修改后的字節碼流
            return writer.toByteArray();
        }
        return null;
    }
}

3.TimeClassVisitor類訪問器,實現字節碼修改,代碼如下:

//定義掃描待修改class的visitor,visitor就是訪問者模式
public class TimeClassVisitor extends ClassVisitor {
    private String className;

    public TimeClassVisitor(ClassVisitor cv, String className) {
        super(Opcodes.ASM5, cv);
        this.className = className;
    }

    //掃描到每個方法都會進入,參數詳情下一篇博文詳細分析
    @Override public MethodVisitor visitMethod(int access, final String name, final String desc, String signature, String[] exceptions) {
        MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
        final String key = className + name + desc;
        //過來待修改類的構造函數
        if (!name.equals("<init>") && mv != null) {
            mv = new AdviceAdapter(Opcodes.ASM5, mv, access, name, desc) {
                //方法進入時獲取開始時間
                @Override public void onMethodEnter() {
                    //相當於com.blueware.agent.TimeUtil.setStartTime("key");
                    this.visitLdcInsn(key);
                    this.visitMethodInsn(Opcodes.INVOKESTATIC, "com/blueware/agent/TimeUtil", "setStartTime", "(Ljava/lang/String;)V", false);
                }

                //方法退出時獲取結束時間並計算執行時間
                @Override public void onMethodExit(int opcode) {
                    //相當於com.blueware.agent.TimeUtil.setEndTime("key");
                    this.visitLdcInsn(key);
                    this.visitMethodInsn(Opcodes.INVOKESTATIC, "com/blueware/agent/TimeUtil", "setEndTime", "(Ljava/lang/String;)V", false);
                    //向棧中壓入類名稱
                    this.visitLdcInsn(className);
                    //向棧中壓入方法名
                    this.visitLdcInsn(name);
                    //向棧中壓入方法描述
                    this.visitLdcInsn(desc);
                    //相當於com.blueware.agent.TimeUtil.getExclusiveTime("com/blueware/agent/TestTime","testTime");
                    this.visitMethodInsn(Opcodes.INVOKESTATIC, "com/blueware/agent/TimeUtil", "getExclusiveTime", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V", false);
                }
            };
        }
        return mv;
    }
}

4.TimeClassVisitor記錄時間幫助類,代碼如下:

public class TimeUtil {
    private static Map<String, Long> startTimes = new HashMap<String, Long>();
    private static Map<String, Long> endTimes   = new HashMap<String, Long>();

    private TimeUtil() {
    }

    public static long getStartTime(String key) {
        return startTimes.remove(key);
    }

    public static void setStartTime(String key) {
        startTimes.put(key, System.currentTimeMillis());
    }

    public static long getEndTime(String key) {
        return endTimes.remove(key);
    }

    public static void setEndTime(String key) {
        endTimes.put(key, System.currentTimeMillis());
    }

    public static void getExclusiveTime(String className, String methodName, String methodDesc) {
        String key = className + methodName + methodDesc;
        long exclusive = getEndTime(key) - getStartTime(key);
        System.out.println(className.replace("/", ".") + "." + methodName + " exclusive:" + exclusive);
    }
}

題記

  • 上面的代碼難免有bug,如果你發現代碼寫的有問題,請你幫忙指出,讓我們一起進步,讓代碼變的更漂亮和健壯;
  • 順便打點廣告,如果看后對字節碼技術感興趣,歡迎加入我們(oneapm)一起做點有意思事情,可直接聯系我;
  • 完整代碼請訪問github;
  • 下一篇結合demo在深入研究ClassVisitor


免責聲明!

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



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