TProfiler是一個能夠在生產環境長期使用的性能分析工具.它同時支持剖析和採樣兩種方式,記錄方法執行的時間和次數,生成方法熱點 對象建立熱點 線程狀態分析等數據,爲查找系統性能瓶頸提供數據支持.
TProfiler在JVM啓動時把時間採集程序注入到字節碼中,整個過程無需修改應用源碼.運行時會把數據寫到日誌文件,通常狀況下每小時輸出的日誌小於50M.
業界同類開源產品都不是針對大型Web應用設計的,對性能消耗較大不能長期使用,TProfiler解決了這個問題.目前TProfiler已應用於淘寶的核心Java前端系統.
部署後低峯期對應用響應時間影響20% 高峯期對吞吐量大約有30%的下降(高峯期能夠遠程關閉此工具)
前端
與同類開源工具jip對比 | ||
---|---|---|
項目 | TProfiler | JIP |
交互控制java |
支持遠程開關和狀態查看git |
支持遠程開關等多種操做github |
過濾包和類名web |
支持包和類的過濾編程 |
支持包和類的過濾數組 |
低消耗數據結構 |
響應時間延長20% QPS下降30%(詳細對比看上圖)框架 |
同等條件下資源消耗較多,使JVM不斷的FullGC;Profile時會阻塞其餘線程異步 |
無本地代碼 |
未使用JVMTI,純Java開發 |
未使用JVMTI,純Java開發 |
易用性 |
只有一個jar包,使用簡單 |
模塊多,配置使用相對複雜 |
日誌文件 |
對日誌進行優化,每小時通常小於50M |
同等條件下日誌大約是TProfiler的8倍,不能自動dump須要客戶端觸發 |
日誌分析 |
目前只提供文本展現 |
能夠利用客戶端分析展現日誌 |
使用場景 |
大型應用/小型應用 長期使用 |
小型應用 短時間使用 |
從圖中能夠看出,該工具使用了java6的Instrumention特性以及asm字節碼修改
利用 Java 代碼,即 java.lang.instrument 作動態 Instrumentation 是 Java SE 5 的新特性,它把 Java 的 instrument 功能從本地代碼中解放出來,使之能夠用 Java 代碼的方式解決問題。使用 Instrumentation,開發者能夠構建一個獨立於應用程序的代理程序(Agent),用來監測和協助運行在 JVM 上的程序,甚至可以替換和修改某些類的定義。有了這樣的功能,開發者就能夠實現更爲靈活的運行時虛擬機監控和 Java 類操做了,這樣的特性實際上提供了一種虛擬機級別支持的 AOP 實現方式,使得開發者無需對 JDK 作任何升級和改動,就能夠實現某些 AOP 的功能了。
在 Java SE 6 裏面,instrumentation 包被賦予了更強大的功能:啓動後的 instrument、本地代碼(native code)instrument,以及動態改變 classpath 等等。這些改變,意味着 Java 具備了更強的動態控制、解釋能力,它使得 Java 語言變得更加靈活多變。
Instrumentation 的基本功能和用法
「java.lang.instrument」包的具體實現,依賴於 JVMTI。JVMTI(Java Virtual Machine Tool Interface)是一套由 Java 虛擬機提供的,爲 JVM 相關的工具提供的本地編程接口集合。JVMTI 是從 Java SE 5 開始引入,整合和取代了之前使用的 Java Virtual Machine Profiler Interface (JVMPI) 和 the Java Virtual Machine Debug Interface (JVMDI),而在 Java SE 6 中,JVMPI 和 JVMDI 已經消失了。JVMTI 提供了一套」代理」程序機制,能夠支持第三方工具程序以代理的方式鏈接和訪問 JVM,並利用 JVMTI 提供的豐富的編程接口,完成不少跟 JVM 相關的功能。事實上,java.lang.instrument 包的實現,也就是基於這種機制的:在 Instrumentation 的實現當中,存在一個 JVMTI 的代理程序,經過調用 JVMTI 當中 Java 類相關的函數來完成 Java 類的動態操做。除開 Instrumentation 功能外,JVMTI 還在虛擬機內存管理,線程控制,方法和變量操做等等方面提供了大量有價值的函數。關於 JVMTI 的詳細信息,請參考 Java SE 6 文檔(請參見 參考資源)當中的介紹。
Instrumentation 的最大做用,就是類定義動態改變和操做。在 Java SE 5 及其後續版本當中,開發者能夠在一個普通 Java 程序(帶有 main 函數的 Java 類)運行時,經過 – javaagent參數指定一個特定的 jar 文件(包含 Instrumentation 代理)來啓動 Instrumentation 的代理程序。
在 Java SE 5 當中,開發者可讓 Instrumentation 代理在 main 函數運行前執行。簡要說來就是以下幾個步驟:
1:編寫 premain 函數
public static void premain(String agentArgs, Instrumentation inst); [1] public static void premain(String agentArgs); [2]
其中,[1] 的優先級比 [2] 高,將會被優先執行([1] 和 [2] 同時存在時,[2] 被忽略)。
在這個 premain 函數中,開發者能夠進行對類的各類操做。
agentArgs 是 premain 函數獲得的程序參數,隨同 「– javaagent」一塊兒傳入。與 main 函數不一樣的是,這個參數是一個字符串而不是一個字符串數組,若是程序參數有多個,程序將自行解析這個字符串。
Inst 是一個 java.lang.instrument.Instrumentation 的實例,由 JVM 自動傳入。java.lang.instrument.Instrumentation 是 instrument 包中定義的一個接口,也是這個包的核心部分,集中了其中幾乎全部的功能方法,例如類定義的轉換和操做等等。
jar 文件打包
2:將這個 Java 類打包成一個 jar 文件,並在其中的 manifest 屬性當中加入」 Premain-Class」來指定步驟 1 當中編寫的那個帶有 premain 的 Java 類。(可能還須要指定其餘屬性以開啓更多功能)
3:運行
用以下方式運行帶有 Instrumentation 的 Java 程序:
java -javaagent:jar 文件的位置 [= 傳入 premain 的參數 ]
對 Java 類文件的操做,能夠理解爲對一個 byte 數組的操做(將類文件的二進制字節流讀入一個 byte 數組)。開發者能夠在「ClassFileTransformer」的 transform 方法當中獲得,操做並最終返回一個類的定義(一個 byte 數組)。
Tprofiler就是經過這種方式來對字節碼進行修改的。
首先假設咱們有一個類HelloWorld, 能夠經過一個方法打印字符串。
public HelloWorld{ public void sayHello(){ System.out.println("hello world!"); } }
咱們須要記錄方法執行的時間和次數須要如何實現?
ProfTransformer是Tprofiler自定義的ClassFileTransformer,用於轉換類字節碼
public class ProfTransformer implements ClassFileTransformer { public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { if (loader != null && ProfFilter.isNotNeedInjectClassLoader(loader.getClass().getName())) { return classfileBuffer; } if (!ProfFilter.isNeedInject(className)) { return classfileBuffer; } if (ProfFilter.isNotNeedInject(className)) { return classfileBuffer; } if (Manager.instance().isDebugMode()) { System.out.println(" ---- TProfiler Debug: ClassLoader:" + loader + " ---- class: " + className); } // 記錄注入類數 Profiler.instrumentClassCount.getAndIncrement(); try { ClassReader reader = new ClassReader(classfileBuffer); ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS); ClassAdapter adapter = new ProfClassAdapter(writer, className); reader.accept(adapter, 0); // 生成新類字節碼 return writer.toByteArray(); } catch (Exception e) { e.printStackTrace(); // 返回舊類字節碼 return classfileBuffer; } } }
這個類實現了 ClassFileTransformer 接口。其中 ClassFileTransformer 當中規定的 transform 方法則完成了類定義的替換轉換。
在代碼中咱們能夠看到兩個類ClassReader和ClassWriter這兩個ASM的字節碼操做API,經過他們咱們能夠對原來的class進行增長行爲。
(ASM是一個操做字節碼(bytecode)的框架,很是的小巧和快速,這個asm-3.3.1.jar,只有43k的大小。asm提供了字節碼的讀寫的功能。而asm的核心,採用的是visitor的模式,提供了ClassReader和ClassWriter這兩個很是重要的類以及ClassVisitor這個核心的接口。)
ASM框架中的核心類有如下幾個:
ClassReader:該類用來解析編譯過的class字節碼文件。
ClassWriter:該類用來從新構建編譯後的類,好比說修改類名、屬性以及方法,甚至能夠生成新的類的字節碼文件。
ClassAdapter:該類也實現了ClassVisitor接口,它將對它的方法調用委託給另外一個ClassVisitor對象。
ClassReader的職責是讀取字節碼。能夠用InputStream、byte數組、類名(須要ClassLoader.getSystemResourceAsStream可以加載到的class文件)做爲構造函數的參數構造ClassReader對象,來讀取字節碼。而ClassReader的工做,就是根據字節碼的規範,從輸入中讀取bytecode。而經過ClassReader對象,獲取bytecode信息有兩種方式,一種就是採用visitor的模式,傳入一個ClassVisitor對象給ClassReader的accept方法。另一種,是使用Low Level的方式,使用ClassReader提供了readXXX以及getXXX的方法來獲取信息。
對於通常使用,用ClassReader的accept方法,使用visitor模式就能夠了。其中ProfClassAdapter繼承了org.objectweb.asm.ClassAdapter。在asm中,ClassAdapter這個類,用asm的javadoc上的話說,就是一個代理到其餘ClassVisitor的一個空的ClassVisitor(An empty ClassVisitor that delegates to another ClassVisitor.)。具體來講,構造ClassAdapter對象的時候,須要傳遞一個ClassVisitor的對象給ClassAdapter的構造函數,而ClassAdapter對ClassVisitor的實現,就是直接調用這個傳給ClassAdapter的ClassVisitor對象的對應visit方法。ClassVisitor,在 ASM3.0 中是一個接口,到了 ASM4.0 與 ClassAdapter 抽象類合併。主要負責 「拜訪」 類成員信息。其中包括(標記在類上的註解,類的構造方法,類的字段,類的方法,靜態代碼塊)
這裏主要介紹其中幾個關鍵方法:
visit(int , int , String , String , String , String[])
該方法是當掃描類時第一個拜訪的方法,主要用於類聲明使用。下面是對方法中各個參數的示意:visit( 類版本 , 修飾符 , 類名 , 泛型信息 , 繼承的父類 , 實現的接口)。
visitAnnotation(String , boolean)
該方法是當掃描器掃描到類註解聲明時進行調用。下面是對方法中各個參數的示意:visitAnnotation(註解類型 , 註解是否能夠在 JVM 中可見)。
visitField(int , String , String , String , Object)
該方法是當掃描器掃描到類中字段時進行調用。下面是對方法中各個參數的示意:visitField(修飾符 , 字段名 , 字段類型 , 泛型描述 , 默認值)。
visitMethod(int , String , String , String , String[])
該方法是當掃描器掃描到類的方法時進行調用。下面是對方法中各個參數的示意:visitMethod(修飾符 , 方法名 , 方法簽名 , 泛型信息 , 拋出的異常)。
visitEnd()
該方法是當掃描器完成類掃描時纔會調用,若是想在類中追加某些方法。能夠在該方法中實現。在後續文章中咱們會用到這個方法。
接下來咱們看在ProfClassAdapter中作了些什麼:
public class ProfClassAdapter extends ClassAdapter { /** * 類名 */ private String mClassName; /** * 文件名 */ private String mFileName = null; /** * 字段對應方法列表 */ private List<String> fieldNameList = new ArrayList<String>(); /* (non-Javadoc) * @see org.objectweb.asm.ClassAdapter#visit(int, int, java.lang.String, java.lang.String, java.lang.String, java.lang.String[]) */ public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { super.visit(version, access, name, signature, superName, interfaces); } /** * @param visitor * @param theClass */ public ProfClassAdapter(ClassVisitor visitor, String theClass) { super(visitor); this.mClassName = theClass; } /* (non-Javadoc) * @see org.objectweb.asm.ClassAdapter#visitSource(java.lang.String, java.lang.String) */ public void visitSource(final String source, final String debug) { super.visitSource(source, debug); mFileName = source; } /* (non-Javadoc) * @see org.objectweb.asm.ClassAdapter#visitField(int, java.lang.String, java.lang.String, java.lang.String, java.lang.Object) */ public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) { String up = name.substring(0, 1).toUpperCase() + name.substring(1, name.length()); String getFieldName = "get" + up; String setFieldName = "set" + up; String isFieldName = "is" + up; fieldNameList.add(getFieldName); fieldNameList.add(setFieldName); fieldNameList.add(isFieldName); return super.visitField(access, name, desc, signature, value); } /* (non-Javadoc) * @see org.objectweb.asm.ClassAdapter#visitMethod(int, java.lang.String, java.lang.String, java.lang.String, java.lang.String[]) */ public MethodVisitor visitMethod(int arg, String name, String descriptor, String signature, String[] exceptions) { if (Manager.isIgnoreGetSetMethod()) { if (fieldNameList.contains(name)) { return super.visitMethod(arg, name, descriptor, signature, exceptions); } } // 靜態區域不注入 if ("<clinit>".equals(name)) { return super.visitMethod(arg, name, descriptor, signature, exceptions); } MethodVisitor mv = super.visitMethod(arg, name, descriptor, signature, exceptions); MethodAdapter ma = new ProfMethodAdapter(mv, mFileName, mClassName, name); return ma; } }
他主要重寫visitField和visitMethod這兩個方法,其中visitField中主要作的事情就是獲得屬性名稱的get和set方法名稱放入集合中,在visitMethod調用的時候先判斷是否過濾get和set方法,不對他們進行加強操做。在執行MethodVisitor mv = super.visitMethod(arg, name, descriptor, signature, exceptions);會得到MethodVisitor對象,經過這個對象能夠對方法訪問進行AOP的代碼注入。
public class ProfMethodAdapter extends MethodAdapter { /** * 方法ID */ private int mMethodId = 0; /** * @param visitor * @param fileName * @param className * @param methodName */ public ProfMethodAdapter(MethodVisitor visitor, String fileName, String className, String methodName) { super(visitor); mMethodId = MethodCache.Request(); MethodCache.UpdateMethodName(mMethodId, fileName, className, methodName); // 記錄方法數 Profiler.instrumentMethodCount.getAndIncrement(); } /* (non-Javadoc) * @see org.objectweb.asm.MethodAdapter#visitCode() */ public void visitCode() { this.visitLdcInsn(mMethodId); this.visitMethodInsn(INVOKESTATIC, "com/taobao/profile/Profiler", "Start", "(I)V"); super.visitCode(); } /* (non-Javadoc) * @see org.objectweb.asm.MethodAdapter#visitLineNumber(int, org.objectweb.asm.Label) */ public void visitLineNumber(final int line, final Label start) { MethodCache.UpdateLineNum(mMethodId, line); super.visitLineNumber(line, start); } /* (non-Javadoc) * @see org.objectweb.asm.MethodAdapter#visitInsn(int) */ public void visitInsn(int inst) { switch (inst) { case Opcodes.ARETURN: case Opcodes.DRETURN: case Opcodes.FRETURN: case Opcodes.IRETURN: case Opcodes.LRETURN: case Opcodes.RETURN: case Opcodes.ATHROW: this.visitLdcInsn(mMethodId); this.visitMethodInsn(INVOKESTATIC, "com/taobao/profile/Profiler", "End", "(I)V"); break; default: break; } super.visitInsn(inst); } }
MethodAdapter類實現了MethodVisitor接口,在MethodVisitor接口中嚴格地規定了每一個visitXXX的訪問順序,以下:
visitAnnotationDefault?( visitAnnotation | visitParameterAnnotation | visitAttribute )*( visitCode( visitTryCatchBlock | visitLabel | visitFrame | visitXxxInsn |visitLocalVariable | visitLineNumber )*visitMaxs )?visitEnd這裏所說的ASM訪問不是指ASM代碼去調用某個類的具體方法,而是指去分析某個類的某個方法的二進制字節碼。
在這裏visitCode方法將會在ASM開始訪問某一個方法時調用,所以這個方法通常能夠用來在進入分析JVM字節碼以前來新增一些字節碼,visitXxxInsn是在ASM具體訪問到每一個指令時被調用,上面代碼中咱們使用的是visitInsn方法,它是ASM訪問到無參數指令時調用的,這裏咱們判但了當前指令是否爲無參數的return來在方法結束前添加一些指令。
經過重寫visitCode和visitInsn兩個方法,咱們就實現了具體的業務邏輯被調用前和被調用後植入監控運行時間的代碼。
咱們看重寫裏面作了什麼:
public ProfMethodAdapter(MethodVisitor visitor, String fileName, String className, String methodName) { super(visitor); mMethodId = MethodCache.Request(); MethodCache.UpdateMethodName(mMethodId, fileName, className, methodName); // 記錄方法數 Profiler.instrumentMethodCount.getAndIncrement(); }
這個構造函數裏面首先調用了MethodCache.Request()佔位並生成方法ID,而後初始化方法信息。
接着是
public void visitCode() { this.visitLdcInsn(mMethodId); this.visitMethodInsn(INVOKESTATIC, "com/taobao/profile/Profiler", "Start", "(I)V"); super.visitCode(); }
重寫這個方法實現了在訪問方法前執行Profiler的靜態方法start,用於開啓時間以及方法信息記錄邏輯。
在而後是
public void visitInsn(int inst) { switch (inst) { case Opcodes.ARETURN: case Opcodes.DRETURN: case Opcodes.FRETURN: case Opcodes.IRETURN: case Opcodes.LRETURN: case Opcodes.RETURN: case Opcodes.ATHROW: this.visitLdcInsn(mMethodId); this.visitMethodInsn(INVOKESTATIC, "com/taobao/profile/Profiler", "End", "(I)V"); break; default: break; } super.visitInsn(inst); }
表示在方法執行結束後執行Profiler的靜態方法End,完成方法調用時間及其餘數據的統計計算。
經過以上方式實現了無變成的AOP注入。
結下來咱們重點看下Profiler的start和end方法是如何將方法作記錄的。
首先回到開始Tprofiler的實現原理中的那張圖片:
其中使用了一個ThreadData數據用來記錄線程性能分析數據。每一個線程有本身的ThreadData
ThreadData的數據結構:
public class ThreadData { /** * 性能分析數據 */ public ProfStack<long[]> profileData = new ProfStack<long[]>(); /** * 棧幀 */ public ProfStack<long[]> stackFrame = new ProfStack<long[]>(); /** * 當前棧深度 */ public int stackNum = 0; /** * 清空數據 */ public void clear(){ profileData.clear(); stackFrame.clear(); stackNum = 0;ssss } }
其中stackFrame是一個自定義棧的數組,包含三個數據,
frameData[0] = methodId;方法ID
frameData[1] = thrData.stackNum;調用數
frameData[2] = startTime;開始時間
thrData.stackFrame.push(frameData);
thrData.stackNum++;
這樣在執行1個方法的前會作一些記錄,並放入自定義棧
在來看看end作了什麼:
/** * 方法退出時調用,採集結束時間 * * @param methodId */ public static void End(int methodId) { if (!Manager.instance().canProfile()) { return; } long threadId = Thread.currentThread().getId(); if (threadId >= size) { return; } long endTime; if (Manager.isNeedNanoTime()) { endTime = System.nanoTime(); } else { endTime = System.currentTimeMillis(); } try { ThreadData thrData = threadProfile[(int) threadId]; if (thrData == null || thrData.stackNum <= 0 || thrData.stackFrame.size() == 0) { // 沒有執行start,直接執行end/多是異步中止致使的 return; } // 棧太深則拋棄部分數據 if (thrData.profileData.size() > 20000) { thrData.stackNum--; thrData.stackFrame.pop(); return; } thrData.stackNum--; long[] frameData = thrData.stackFrame.pop(); long id = frameData[0]; if (methodId != id) { return; } long useTime = endTime - frameData[2]; if (Manager.isNeedNanoTime()) { if (useTime > 500000) { frameData[2] = useTime; thrData.profileData.push(frameData); } } else if (useTime > 1) { frameData[2] = useTime; thrData.profileData.push(frameData); } } catch (Exception e) { e.printStackTrace(); } }
其實挺簡單的,就是在方法執行結束後出棧,獲取到其對應的開始記錄的數據,而後作統計和計算,在吧結果放入到性能分析數據中去。 大致上這個就是Tprofiler的實現原理。至於在外面如何得到這些數據,在後續的文章中在說吧。