不管是使用 System.out.println()
仍是 hprof
或 OptimizeIt 這樣的監測工具,代碼監測都應當是軟件開發實踐的關鍵部分。這篇文章討論了代碼監測最多見的方式,並解釋了它們的不足。本文提供了對於理想的內部監測器來 說最合適的特性,並解釋了爲何面向方面編程技術很是適於實現其中的一些特性。本文還介紹了 JDK 5.0 代理接口,並詳細介紹了用它構建本身的面向方面的監測器的步驟。java
請注意,這篇文章的示例監測器和 完整源代碼 基於 Java 交互監測器(JIP)—— 一個用面向方面技術和 Java 5 代理接口構建的開放源碼監測器。請參閱 參考資料 學習關於 JIP 和本文中討論的其餘工具的更多內容。web
監測工具和技術apache
多數 Java 開發人員都是從使用 System.currentTimeMillis()
和 System.out.println()
開始測量應用程序性能的。System.currentTimeMillis()
易於使用:只要測量方法開始和結束的時間,並輸出時間差便可,可是它有兩個重大不足:編程
- 它是個手工過程,要求開發人員肯定要測量哪一個代碼;插入工具代碼;從新編譯、從新部署、運行並分析結果;而後在結束時取消工具代碼;而在下次出現 問題時再次重複以上全部步驟。
- 並且它對於應用程序各部分的執行狀況沒有提供全面的觀察。
爲了解決這些問題,有些開發人員轉向 hprof
、JProbe 或 OptimizeIt 這樣的監測器。監測器避免了與即時測量相關聯的問題,由於沒必要修改程序就可使用它們。它們還爲程序性能提供了更全面的觀察,由於它們收集每一個方法調用的 計時信息,而不只僅是某個具體代碼段的計時信息。不幸的是,監測工具也有不足。服務器
監測器的侷限數據結構
監測器對於 System.currentTimeMillis()
這樣的手工解決方案提供了很好的替代,可是它們還遠談不上理想。有一件事,就是用 hprof
運行程序,會把程序減慢 20 倍。這意味着正常狀況下只須要一小時的一個 EFL(提取、轉換、裝入)操做,可能要花一成天才能監測!不只等候是不方便的,並且應用程序時間範圍的改變,實際上也會扭曲結果。以作許多 I/O 操做的程序爲例。由於 I/O 由操做系統執行,監測不會減慢它,因此 I/O 操做看起來運行得要比實際的速度快 20 倍!因此,不能老是依靠 hprof
提供對應用程序性能的正確描述。架構
hprof
的另外一個問題與 Java 程序裝入和運行的方式有關。與 C 或 C++ 這樣的靜態連接語言不一樣,Java 程序是在運行時而不是在編譯時連接的。直到第一次引用的時候,JVM 才裝入類,而代碼直到執行了許屢次以後,才從字節碼編譯成機器碼。若是想測量一個方法的性能,可是它的類尚未裝入,那麼測量就會包含類的裝入時間和編譯 時間再加上運行時間。由於這些事只在應用程序生命開始的時候發生一次,因此若是要測量長期的應用程序性能,一般不想把這些事包含在內。app
當代碼在應用服務器或 servlet 引擎中運行的時候,事情會變得更加複雜。hprof
這樣的監測器會監測整個應用程序、servlet 容器和全部的東西。問題是,一般不想 監測 servlet 引擎,只想監測應用程序。框架
理想的監測器工具
像選擇其餘工具同樣,選擇監測器也有機會成本。hprof
易於使用,但有侷限性,例如不能從監測中過濾掉類或包。商業工具提供了更多特性,可是昂貴並且有嚴格的許可條款。有些監測器要求經過監測器啓動應用程序, 這意味着要用不熟悉的工具從新構建執行環境。監測器的選擇涉及妥協,因此理想的監測器看起來應當像什麼呢?下面是應當追尋的特性的一個簡短列表:
- 速度:監測可能會慢得讓人痛苦。可是可使用不自動監測每一個類的監測器,以便加快速度。
- 交互性:監測器容許的交互越多,對監測器獲得的信息進行的精細調整就越多。例如,可以在運行時開啓和關閉監測器,有助於避免測量類 的裝入、編譯和解釋執行(預 JIT)時間。
- 過濾:根據類或包進行過濾,能夠把注意力集中在手頭的問題上,而不會被太多的信息擾亂。
- 100% 純 Java 代碼:多數監測器都要求使用本機庫,這限制了可使用它們的平臺。理想的監測器不該當要求使用本機庫。
- 開放源碼:開放源碼工具一般容許迅速地起步和運行,同時避免了商業許可的限制。
本身構建監測器!
用 System.currentTimeMillis()
生成計時信息的問題是它是一個手工過程。若是可以自動插入工具代碼,那麼它的許多不足就煙消雲散了。這類問題正是面向方面解決方案最適合解決的問題。對於 構建面向方面的監測器來講,Java 5 引入的代理接口很是理想,由於它提供了掛接到類裝入器和在類裝入時修改類的方便途徑。
本文的剩餘部分集中在 BYOP (構建本身的監測器)上。我將介紹代理接口,並演示如何建立簡單代理。將學習基本監測方面的代碼,以及爲了更高級的監測對它進行修改所採起的步驟。
建立代理
不幸的是,-javaagent
這個 JVM 選項的文檔只有零星記載。找不到太多關於這個主題的書(沒有 Java 代理傻瓜書 或 21 天學會 Java 代理),可是能夠在 參考資料 一節中發現一些好的資源,還有這裏的概述。
代理背後的想法是:在 JVM 裝入類時,代理能夠修改類的字節碼。能夠用三個步驟建立代理:
- 實現
java.lang.instrument.ClassFileTransformer
接口:
public interface ClassFileTransformer { public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException; }
|
- 建立 「premain」 方法。這個方法在應用程序的
main()
方法以前調用,看起來像這樣:
package sample.verboseclass; public class Main { public static void premain(String args, Instrumentation inst) { ... } }
|
- 在代理的 JAR 文件中,包含一個清單條目,表示包含
premain()
方法的類:
Manifest-Version: 1.0 Premain-Class: sample.verboseclass.Main
|
一個簡單的代理
構建監測器的第一步是建立一個代理,在裝入每一個類的時候輸出類的名稱,與 -verbose:class
JVM 選項的功能相似。如清單 1 所示,這隻要求幾行代碼:
清單 1. 一個簡單的代理
package sample.verboseclass; public class Main { public static void premain(String args, Instrumentation inst) { inst.addTransformer(new Transformer()); } } class Transformer implements ClassFileTransformer { public byte[] transform(ClassLoader l, String className, Class<?> c, ProtectionDomain pd, byte[] b) throws IllegalClassFormatException { System.out.print("Loading class: "); System.out.println(className); return b; } }
|
若是代理被打包在叫做 vc.jar
的 JAR 文件中,就應當用 -javaagent
選項啓動 JVM,以下所示:
java -javaagent:vc.jar MyApplicationClass
|
監測方面
有了代理的基本元素以後,下一步就是在裝入應用程序的類時向其中添加簡單的監測方面。幸運的是,不須要掌握修改字節碼的 JVM 指令集的細節。相反,能夠用 ASM 庫這樣的工具包(來自 ObjectWeb 論壇,請參閱 參考資料) 來處理類文件格式的細節。ASM 是個 Java 字節碼操縱框架,使用訪客模式實現對類文件的轉換,使用的方式很是像使用 SAX 事件遍歷和轉換 XML 文檔那樣。
清單 2 中的監測方面能夠用來輸出類名稱、方法名稱和 JVM 每次進入或離開一個方法的時間戳。(對於更復雜的監測器,可能還想使用精度更高的計時器,像 Java 5 的 System.nanoTime()
。)
清單 2. 簡單的監測方面
package sample.profiler; public class Profile { public static void start(String className, String methodName) { System.out.println(new StringBuilder(className) .append('\t') .append(methodName) .append("\tstart\t") .append(System.currentTimeMillis())); } public static void end(String className, String methodName) { System.out.println(new StringBuilder(className) .append('\t') .append(methodName) .append("\end\t") .append(System.currentTimeMillis())); } }
|
若是手工進行監測,那麼下一步多是把每一個方法修改爲像下面這樣:
void myMethod() { Profile.start("MyClass", "myMethod"); ... Profile.end("MyClass", "myMethod"); }
|
使用 ASM 插件
如今須要找出 Profile.start()
和 Profile.end()
調用的字節碼是什麼樣的 —— 這正是 ASM 庫發揮做用的地方。ASM 有一個用於 Eclipse 的 Bytecode Outline 插件(請參閱 參考資料), 它容許查看類或方法的字節碼。圖 1 顯示了以上方法的字節碼。(也可使用 javap
這樣的反彙編器,它是 JDK 的一部分。)
圖 1. 用 ASM 插件查看字節碼
ASM 插件甚至還生成了可以用來生成對應字節碼的 ASM 代碼,如圖 2 所示:
圖 2. ASM 插件生成的代碼
能夠把圖 2 中高亮的代碼複製到代理中,調用 Profile.start()
方法的通用化版本,如清單 3 所示:
清單 3. 插入對監測器的調用的 ASM 代碼
visitLdcInsn(className); visitLdcInsn(methodName); visitMethodInsn(INVOKESTATIC, "sample/profiler/Profile", "start", "(Ljava/lang/String;Ljava/lang/String;)V");
|
爲了插入開始和結束調用,請繼承 ASM 的 MethodAdapter
,如清單 4 所示:
清單 4. 插入對監測器的調用的 ASM 代碼
package sample.profiler; import org.objectweb.asm.MethodAdapter; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; import static org.objectweb.asm.Opcodes.INVOKESTATIC; public class PerfMethodAdapter extends MethodAdapter { private String className, methodName; public PerfMethodAdapter(MethodVisitor visitor, String className, String methodName) { super(visitor); className = className; methodName = methodName; } public void visitCode() { this.visitLdcInsn(className); this.visitLdcInsn(methodName); this.visitMethodInsn(INVOKESTATIC, "sample/profiler/Profile", "start", "(Ljava/lang/String;Ljava/lang/String;)V"); super.visitCode(); } 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(className); this.visitLdcInsn(methodName); this.visitMethodInsn(INVOKESTATIC, "sample/profiler/Profile", "end", "(Ljava/lang/String;Ljava/lang/String;)V"); break; default: break; } super.visitInsn(inst); } }
|
把這個功能掛接到代理的代碼很是簡單,也是這篇文章的 源代碼下載 的一部分。
裝入 ASM 類
由於代理使用 ASM,因此須要確保裝入了 ASM 類,全部東西才能工做。在 Java 應用程序中有許多類路徑:應用程序類路徑、擴展類路徑和啓動類路徑。使人驚訝的是,ASM JAR 沒有采用其中任何一個路徑;相反,要使用清單告訴 JVM 代理須要哪一個 JAR 文件,如清單 5 所示。在這種狀況下,JAR 文件必須與代理的 JAR 放在同一目錄中。
清單 5. 監測器的清單文件
Manifest-Version: 1.0 Premain-Class: sample.profiler.Main Boot-Class-Path: asm-2.0.jar asm-attrs-2.0.jar asm-commons-2.0.jar
|
運行監測器
全部東西都編譯打包以後,就能夠對任何 Java 應用程序運行監測器了。清單 6 中的部分輸出來自對 Ant 的監測,這個 Ant 執行 build.xml 對代理進行編譯:
清單 6. 監測器的輸出示例
org/apache/tools/ant/Main runBuild start 1138565072002 org/apache/tools/ant/Project <init> start 1138565072029 org/apache/tools/ant/Project$AntRefTable <init> start 1138565072031 org/apache/tools/ant/Project$AntRefTable <init> end 1138565072033 org/apache/tools/ant/types/FilterSet <init> start 1138565072054 org/apache/tools/ant/types/DataType <init> start 1138565072055 org/apache/tools/ant/ProjectComponent <init> start 1138565072055 org/apache/tools/ant/ProjectComponent <init> end 1138565072055 org/apache/tools/ant/types/DataType <init> end 1138565072055 org/apache/tools/ant/types/FilterSet <init> end 1138565072055 org/apache/tools/ant/ProjectComponent setProject start 1138565072055 org/apache/tools/ant/ProjectComponent setProject end 1138565072055 org/apache/tools/ant/types/FilterSetCollection <init> start 1138565072057 org/apache/tools/ant/types/FilterSetCollection addFilterSet start 1138565072057 org/apache/tools/ant/types/FilterSetCollection addFilterSet end 1138565072057 org/apache/tools/ant/types/FilterSetCollection <init> end 1138565072057 org/apache/tools/ant/util/FileUtils <clinit> start 1138565072075 org/apache/tools/ant/util/FileUtils <clinit> end 1138565072076 org/apache/tools/ant/util/FileUtils newFileUtils start 1138565072076 org/apache/tools/ant/util/FileUtils <init> start 1138565072076 org/apache/tools/ant/taskdefs/condition/Os <clinit> start 1138565072080 org/apache/tools/ant/taskdefs/condition/Os <clinit> end 1138565072081 org/apache/tools/ant/taskdefs/condition/Os isFamily start 1138565072082 org/apache/tools/ant/taskdefs/condition/Os isOs start 1138565072082 org/apache/tools/ant/taskdefs/condition/Os isOs end 1138565072082 org/apache/tools/ant/taskdefs/condition/Os isFamily end 1138565072082 org/apache/tools/ant/util/FileUtils <init> end 1138565072082 org/apache/tools/ant/util/FileUtils newFileUtils end 1138565072082 org/apache/tools/ant/input/DefaultInputHandler <init> start 1138565072084 org/apache/tools/ant/input/DefaultInputHandler <init> end 1138565072085 org/apache/tools/ant/Project <init> end 1138565072085 org/apache/tools/ant/Project setCoreLoader start 1138565072085 org/apache/tools/ant/Project setCoreLoader end 1138565072085 org/apache/tools/ant/Main addBuildListener start 1138565072085 org/apache/tools/ant/Main createLogger start 1138565072085 org/apache/tools/ant/DefaultLogger <clinit> start 1138565072092 org/apache/tools/ant/util/StringUtils <clinit> start 1138565072096 org/apache/tools/ant/util/StringUtils <clinit> end 1138565072096
|
|
|
跟蹤調用堆棧
迄今爲止,已經看到了如何只用幾行代碼就構建了一個簡單的面向方面的監測器。雖然是個好的開始,可是示例監測器沒有收集線程和調用堆棧數據。調用堆 棧信息對於判斷方法的毛執行時間和淨執行時間是必需的。另外,每一個調用堆棧都與一個線程相關,因此若是想跟蹤調用堆棧數據,也須要線程信息。多數監測器使 用兩趟式設計進行這類分析:首先收集數據,而後分析數據。我將介紹如何採用這種技術,而不是在收集數據的時候輸出數據。
修改監測類
能夠很容易地加強 Profile
類,讓它捕獲堆棧和線程信息。對於初學者來講,不用在每一個方法調用的開始和結束時都輸出時間信息,能夠用圖 3 所示的數據結構保存這些信息:
圖 3. 跟蹤調用堆棧和線程信息的數據結構
有許多方法能夠收集關於調用堆棧的信息。其中之一是實例化一個 Exception
,可是若是在每一個方法的開始和結束時 都作這件事,就太慢了。更簡單的方法是讓監測器管理它本身的內部堆棧。這很容易,由於對於每一個方法都要調用 start()
; 惟一的技巧就是當拋出異常時就解開內部調用堆棧。在調用 Profile.end()
時,經過檢查預期的類和方法名稱,能夠探測到何時拋出了異常。
輸出的設置也很容易。能夠用 Runtime.addShutdownHook()
登記一個 Thread
來建立一個 shutdown 鉤子,在關閉的時候運行,向控制檯輸出監測報告。
結束語
這篇文章介紹了監測目前最經常使用的工具和技術,並討論了它們的一些侷限性。還提供了一個理想的監測器應當具備的特性列表。最後,學習瞭如何用面向方面編程和 Java 5 代理接口構建出集成了一些理想特性的本身的監測器。
這篇文章的示例代碼基於 Java 交互式監測器,這是一個用這裏討論的技術構建的開放源碼監測器。除了示例監測器中的基本特性以外,JIP 還集成了如下特性:
- 交互式監測
- 排除類或包的能力
- 只包含由特定類裝入器裝入的類的能力
- 跟蹤對象分配的工具
- 代碼監測以外的性能測量
JIP 是在 BSD 形式的許可下分發的。請參閱 參考資料 得到下載信息。