1. 程式人生 > >如何編寫屬於自己的Java / Scala的偵錯程式

如何編寫屬於自己的Java / Scala的偵錯程式

譯者:賴輝強  原文地址

在本帖中,我們將探討Java和Scala的偵錯程式是如何編寫和工作的;系統自帶的偵錯程式,例如:Windows中的WinDbg或者是Linux/Unix中的gdb,會獲取作業系統直接提供給他們的連結入口來啟動,從而指導和操作外部程式的狀態。工作在作業系統頂部抽象層的Java虛擬機器對位元組碼的除錯有獨立的處理架構。

這個除錯的框架和APIs具有全開源、文件化、可擴充套件的特點,這意味著你可以輕鬆毫無顧忌的編寫自己的偵錯程式。該框架當前的設計由兩大部分構成—JDWP協議和JVMTI API層。其中每個都有一系列使效能最佳的優點和使用案例。(你也可以閱讀這篇文件:深入 JAVA 除錯體系

JDWP協議

JDWP(Java Debugger Wire Protocol)是用來在除錯和被除錯程式之間通過二進位制資訊來傳遞請求和接收事件的(例如:執行緒中的狀態或者異常的變化),這些活動通常是網路上進行。這個架構背後的理念是在兩個程式之間儘可能的解耦。旨在減少由編譯器更改目的碼在執行期的執行所帶來的海森堡效應(Werner是位德國物理學家,不是你喜歡的那個廚師Werner)。

從目標程式中移除多數除錯邏輯操作,對檢測被除錯的虛擬機器中狀態的改變會有所幫助(例如:GC or OutOfMemoryErrors),這些邏輯是不會除錯本身的。為了更加簡便,JDK自帶了JDI(java除錯介面),該介面提供了全面的除錯的協議實現,以及對一個目標虛擬機器狀態的完備的操作能力,包括:連線、斷開、指導、處理。

Eclipse的編譯器使用的就是JDWP協議,IDE( Integrated Development Environment )除錯JAVA程式時,如果你檢視當時傳遞給該程式的命令列引數,你會發現Eclipse會傳遞額外的引數(-agentlib:jdwp=transport=dt_socket,…)給程式來啟動java虛擬機器除錯,同時也將確定傳送請求和事件的埠。

JVMTI程式設計介面

一系列的原生API是現代JVM中的第二個關鍵元件,這些API涵蓋了廣泛關於JVM操作的領域,其中為人所熟知的是 JVM Tooling Interface (i.e. JVMTI)。與JDWP不同的是,JVMTI設計時提供了一系列C/C++ 版的API和一種為JVM動態地載入預編譯的庫檔案(如:.dull等)的機制,而這些庫檔案會使用由API提供的命令。

JVMTI的使用方式不同於JDWP,實際上,它是在目標程式內執行編譯器。這種方式使偵錯程式同時在效能和穩定性方面改善程式程式碼更加得心應手。然而,最關鍵的優勢是這樣一種能夠幾乎是實時直接地和JVM互動的能力。

從JVMTI提供了一系列功能強、易入門的API中可以看出,JVMTI樂於去深入探究並分析自身的工作原理以及同通過用該些API所能完成的功能。你可以從JDK自帶的JVMTI中獲取API標頭。

編寫偵錯程式類庫

編寫自己的偵錯程式需要用C++建立本地的作業系統類庫。你的主方法應該如下:

JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM *vm,char*options,void*)

當JVM載入除錯代理器的同時,它會呼叫該方法。傳遞的Java虛擬機器指標是至關重要的,它會提供所有你需要跟JVM打交道的砝碼。該指標可以從java虛擬機器中引入jvmtiEnv類;你可以使用GetEnv方法利用capabilities (特性)和events(事件)的概念與JVMTI層進行互動。

JVMTI 特性

編寫偵錯程式時,最關鍵的一方面是你對在目標程式中的偵錯程式程式碼的功能有清晰的認識,特別是執行程式碼的本地偵錯程式類庫和執行程式聯絡緊密時。為了你更好的控制你的偵錯程式去影響程式碼的執行,因此JVMTI詳解中引入了capabilities(特性)的概念。

當編寫自己的編譯器時,你可以事先通知java虛擬機器你一系列打算使用的API命令或者事件(例如:設定斷點、中斷執行緒)。這能夠使JVM可以預先為這些命令或者事件做好準備,同時,讓你更好的掌控偵錯程式執行期的開銷。這種方式也使得出自不同製造商的JVM能夠以程式設計的方式告訴你那些API的命令可以在整個JVMTI詳解中得以支援。

特性的效能是大不相同的。有些特性使用的效能開銷較低,但是有些較有意思的特性則是相反,例如:在程式碼中丟擲異常來接收回調的特性—can_generate_exception_events或者是需要加鎖來接收回調的特性—can_generate_monitor_events。原因在於這些特性會在 JIT全範圍的編譯時阻礙JVM優化程式碼,與此同時,迫使JVM在執行期降到解釋模式。

其它一些特性,如:每當設定一個目標物件域時,用來接收通知的特性—can_generate_field_modification_events,會產生更大的開銷,導致程式碼執行極慢。儘管JVM支援同時載入多個本地類庫,遺憾是一些 HotSpot的特性,如:用來掛起和喚醒執行緒的特性—can_suspend,只能每次地被一個類庫呼叫。

當我們搭建Takipi’s production debugger時,我們需攻堅的問題之一是提供類似的特性且不能引起大的開銷(在之後的版本中更是這樣)。

設定回撥。一旦你接收到一系列的特性後,你隨即設定好會被JVM呼叫的回撥,這會讓你知道實際發生過的操作。每個回撥都將會完全地提供關於已經發生過的事件的深層次資訊。舉個例子,一則異常回調資訊會包括丟擲異常的位元組碼位置、執行緒、異常物件、異常是否將被捕獲以及將被捕獲的位置。

voidJNICALL ExceptionCallback(jvmtiEnv *jvmti,JNIEnv *jni, jthread thread, jmethodID method,

jlocation location, jobject exception,jmethodID catch_method, jlocation catch_location)

值得注意的是特性的開銷通常分為兩個部分,第一部分開銷來自驅動它工作,因為它需要使JIT編譯器不同地編譯事務,從而能夠訪問程式碼。另外一部分來自當你啟用一個回撥功能時,此時這會引起JVM在執行期選擇低效能的執行路徑,通過這些路徑,特性可以訪問程式碼,期間壓縮和傳遞重要資料會產生額外的開銷

斷點和檢查。你的編譯器能夠提供熟知的用來檢查在執行期所處的特定狀態的特性,如:SetBreakpoint,通知JVM通過某個具體的位元組碼來中斷執行,或者每當某個區域更改時,通過設定SetFieldModificationWatch中斷執行。針對這點,你可以使用其它一些補充性的功能,例如:GetStackTrace 和GetThreadInfo ,用來知曉當前你所在程式碼中的位置並報告當前位置。

大多數JVMTI 的功能都涉及到一些使用抽象處理的類和方法,較為熟知的是jmethodID 和 jclass(如果你曾經編寫過java本地介面程式碼,這是)。其中也提供了額外的一些功能,如:GetMethodName 和 GetClassSignature,來幫助你從類常量池中獲取實際的符號名。之後,你就可以使用這些符號名以可讀的方式記錄為日誌檔案或者以介面的方式展示它們,就如同日常在IDE中所見的一樣。

連線偵錯程式

一旦你已經開始編寫偵錯程式類庫,你的下一步任務就是將它連線到JVM上,下面是幾種連線的方式:

1. 連線JDWP。倘若你編寫的是一個以JDWP為基礎的偵錯程式,你需要向被除錯物件增加一個啟動引數,就可以在線上進行除錯,該引數的形式如下:agentlib:jdwp=transport=dt_socket,suspend=y,address=localhost:<port>。這些引數詳細反映了偵錯程式和目標程式之間傳遞資訊的方式,以及在掛起模式中是否啟用被除錯物件。

2. 連線JVMTI 類庫.通過傳遞給被除錯程式的代理路徑命令列,同時指向類庫所在硬碟上的位置,此時,JVM將會載入JVMTI類庫。

另外一種可行的方式是:將命令列引數追加到全域性環境變數JAVA_TOOL_OPTIONS 後面,每個新的JVM會接收該變數,並且該變數的值會自動地追加到現存引數列表之後。

3. 遠端連線.還有一種通過使用遠端連線API來連線偵錯程式的方式,使用這個簡單而功能強大的API能夠在沒有使用任何命令列引數來開始程式的情況下連線代理器來執行JVM程式。這個的不足在於你不能獲取通常本可以獲得的特性,如:can_generate_exception_events,因為這些特性只能在虛擬機器啟動時獲取。