淺談JPDA中JVMTI模塊

0 前言

上一節《Java Instrument 功能使用及原理》文章中,講解Instrument使用時,簡單提了一句JVMTI的概念,可能有不少小夥伴感到很陌生,雖然Java Instrument的使用基本沒什麼問題,但對於Instrument基於JVMTI的實現原理仍是處於混沌狀態。因此本節的重點就在於講解JVMTI,正如標題。前端

但因爲JVMTI在整個JVM JPDA體系中只是其中的一個小模塊,爲了使你們在總體上能有個清晰的認識,那咱們先從JPDA體系開始吧。java

1 JPDA 介紹

全部的程序員都會遇到 bug,對於運行態的錯誤,咱們每每須要一些方法來觀察和測試運行態中的環境。做爲一個合格的Developer,最基本的技能就是要掌握語言在不一樣IDE的Debug技能。linux

Intellij IDEA 就提供一個功能很是全面,操做很是簡單的調試器,以下圖:c++

Intellij IDEA 調試界面

有時甚至不用 IDE 提供的圖形界面,使用 JDK 自帶的 jdb 工具,以文本命令的形式來調試您的 Java 程序。這些形形色色的調試器都 支持本地和遠程的程序調試,那麼它們是如何被開發的?它們之間存在着什麼樣的聯繫呢?程序員

咱們不得不說起 Java 的調試體系—— JPDA,它是咱們通向虛擬機,考察虛擬機運行態的一個通道,一套工具算法

Java 程序都是運行在 Java 虛擬機上的,咱們要調試 Java 程序,事實上就須要向 Java 虛擬機請求當前運行態的狀態,並對虛擬機發出必定的指令,設置一些回調等等,那麼 Java 的調試體系——JPDA,就是虛擬機的一整套用於調試的工具和接口編程

顧名思義,這個體系爲開發人員提供了 一整套用於調試 Java 程序的 API,是一套用於開發 Java 調試工具的接口和協議windows

經過這些 JPDA 提供的接口和協議,調試器開發人員就能根據特定開發者的需求,擴展定製 Java 調試應用程序,開發出吸引開發人員使用的調試工具。後端

但咱們要注意的是,JPDA 是一套標準,任何的 JDK 實現都必須完成這個標準,所以,經過 JPDA 開發出來的調試工具先天 具備跨平臺、不依賴虛擬機實現、JDK 版本無關等移植優勢,所以大部分的調試工具都是基於這個體系。api

1.1 JPDA 模塊

JPDA 定義了一個完整獨立的體系,它由三個相對獨立的層次共同組成,並且規定了它們三者之間的交互方式,或者說定義了它們通訊的接口。

三個層次由低到高分別是 Java 虛擬機工具接口(JVMTI),Java 調試協議(JDWP)以及 Java 調試接口(JDI)。這三個模塊把調試過程分解成幾個很天然的概念:調試者(debugger)和被調試者(debuggee),以及他們中間的通訊器

被調試者(JVMTI):運行於咱們想調試的 Java 虛擬機之上,它能夠經過 JVMTI 這個標準接口,監控當前虛擬機的信息

調試者(JDI):定義了用戶可以使用的調試接口,經過這些接口,用戶能夠對被調試虛擬機發送調試命令,同時調試者接受並顯示調試結果

中間通訊器(JDWP)在調試者和被調試者之間,調試命令和調試結果,都是經過 JDWP 的通信協議傳輸的;全部的命令被封裝成 JDWP 命令包,經過傳輸層發送給被調試者,被調試者接收到 JDWP 命令包後,解析這個命令並轉化爲 JVMTI 的調用,在被調試者上運行;相似的,JVMTI 的運行結果,被格式化成 JDWP 數據包,發送給調試者並返回給 JDI 調用。而調試器開發人員就是經過 JDI 獲得數據,發出指令;

上述整個過程,以下圖所示:

JPDA 模塊層次

固然,開發人員徹底能夠不使用完整的三個層次,而是 基於其中的某一個層次開發本身的應用。好比:徹底能夠僅僅依靠經過 JVMTI 函數開發一個調試工具,而不使用 JDWP 和 JDI,只使用本身的通信和命令接口。固然,除非是有特殊的需求,利用已有的實現會事半功倍,避免重複發明輪子。

下面,咱們就分別講解下JPDA的三種組成模塊:

  1. Java 虛擬機工具接口(JVMTI)

    JVMTI(Java Virtual Machine Tool Interface)即指 Java 虛擬機工具接口,它是 一套由虛擬機直接提供的 native 接口,它處於整個 JPDA 體系的最底層,全部調試功能本質上都須要經過 JVMTI 來提供。經過這些接口,開發人員不只調試在該虛擬機上運行的 Java 程序,還能 查看它們運行的狀態,設置回調函數,控制某些環境變量,從而優化程序性能。

  2. Java 調試線協議(JDWP)

    JDWP(Java Debug Wire Protocol)是一個爲 Java 調試而設計的一個通信交互協議,它定義了調試器和被調試程序之間傳遞的信息的格式

    在 JPDA 體系中,做爲前端(front-end)的調試者(debugger)進程和後端(back-end)的被調試程序(debuggee)進程之間的交互數據的格式就是由 JDWP 來描述的,它詳細完整地定義了請求命令、迴應數據和錯誤代碼,保證了前端和後端的 JVMTI 和 JDI 的通訊通暢。

    好比:在 Sun 公司提供的實現中,它提供了一個名爲 jdwp.dll(jdwp.so)的動態連接庫文件,這個動態庫文件實現了一個 Agent,它會負責解析前端發出的請求或者命令,並將其轉化爲 JVMTI 調用,而後將 JVMTI 函數的返回值封裝成 JDWP 數據發還給後端

    另外,這裏須要注意的是 JDWP 自己並不包括傳輸層的實現,傳輸層須要獨立實現,可是 JDWP 包括了和傳輸層交互的嚴格的定義,就是說,JDWP 協議雖然不規定咱們是經過 EMS 仍是快遞運送貨物的,可是它規定了咱們傳送的貨物的擺放的方式。在 Sun 公司提供的 JDK 中,在傳輸層上,它提供了 socket 方式,以及在 Windows 上的 shared memory 方式。固然,傳輸層自己無非就是本機內進程間通訊方式和遠端通訊方式,也能夠按 JDWP 的標準本身實現

  3. Java 調試接口(JDI)

    JDI(Java Debug Interface)是三個模塊中最高層的接口,在多數的 JDK 中,它是由 Java 語言實現的。 JDI 由針對前端定義的接口組成,經過它,調試工具開發人員就能經過前端虛擬機上的調試器來遠程操控後端虛擬機上被調試程序的運行,JDI 不只能幫助開發人員格式化 JDWP 數據,並且還能爲 JDWP 數據傳輸提供隊列、緩存等優化服務。從理論上說,開發人員只需使用 JDWP 和 JVMTI 便可支持跨平臺的遠程調試,可是直接編寫 JDWP 程序費時費力,並且效率不高。所以基於 Java 的 JDI 層的引入,簡化了操做,提升了開發人員開發調試程序的效率

JPDA 層次比較

1.2 JPDA 實現

每個虛擬機都應該實現 JVMTI 接口,可是 JDWP 和 JDI 自己與虛擬機並不是是不可分的,這三個層之間是經過標準所定義的交互的接口和協議聯繫起來的,所以它們能夠被獨立替換或取代,但不會影響到總體調試工具的開發和使用。所以,開發和使用本身的 JDWP 和 JDI 接口實現是可能的。

Java 軟件開發包(SDK)標準版裏提供了 JPDA 三個層次的標準實現,事實上,調試工具開發人員還有不少其餘開源實現能夠選擇,好比 Apache Harmony 提供了 JDWP 的實現。而 JDI,咱們能夠在 Eclipse 一個子項目 org.eclipse.jdt.debug 裏找到其完整的實現(Harmony 也使用了這套實現,做爲其 J2SE 類庫的一部分)。經過標準協議,Eclipse IDE 的調試工具就能夠徹底在 Harmony 的環境上運行。

2 JVMTI 介紹

JVMTI(JVM Tool Interface)是 Java 虛擬機所提供的 native 編程接口,是 JVMPI(Java Virtual Machine Profiler Interface)和 JVMDI(Java Virtual Machine Debug Interface)的替代版本。

從這個 API 的替代軌跡中可知,JVMTI 提供了可用於 debug 和 profiler 的接口;同時,在 Java 5/6 中,JVMTI 接口也增長了 監聽(Monitoring),線程分析(Thread analysis)以及覆蓋率分析(Coverage Analysis) 等功能。正是因爲 JVMTI 的強大功能,它是實現 Java 調試器,以及其它 Java 運行態測試與分析工具的基礎。

JVMTI 是一套本地代碼接口,可使開發者直接與 C/C++ 以及 JNI 打交道

那麼,開發者是如何來使用JVMTI所提供的接口呢?事實上,通常採用創建一個 Agent 的方式來使用 JVMTI,這個Agent的表現形式是一個以c/c++語言編寫的動態連接庫

把 Agent 編譯成一個動態連接庫,Java啓動或運行時,動態加載一個外部基於JVMTI 編寫的dynamic module到Java進程內,而後觸發 JVM源生線程Attach Listener來執行這個dynamic module的回調函數

在回調函數體內,能夠 獲取各類各樣的VM級信息,註冊感興趣的VM事件,甚至控制VM行爲

2.1 Agent 工做過程

2.1.1 啓動

JVMTI有兩種啓動方式,第一種是隨Java進程啓動時,自動載入共享庫,下文簡稱 啓動時載入。另外一種方式是,Java運行時,經過attach api動態載入,下文簡稱 運行時載入

啓動時載入,經過在Java命令行啓動時傳遞一個特殊的option,以下:

  1. java -agentlib:= Sample 注意,這裏的共享庫路徑是環境變量路徑,例如 java -agentlib:foo=opt1,opt2,java啓動時會從linux的LD_LIBRARY_PATH或windows的PATH環境變量定義的路徑處裝載foo.so或foo.dll,找不到則拋異常

  2. java -agentpath:= Sample 這是 以絕對路徑的方式裝載共享庫,例如 java -agentpath:/home/admin/agentlib/foo.so=opt1,opt2

啓動時載入,處於虛擬機初始化的早期,在這個時間點上:

  1. 全部的 Java 類都未被初始化;
  2. 全部的 Java 對象實例都未被建立;
  3. 於是,沒有任何 Java 代碼被執行;

但在這個時候,咱們已經能夠:

  1. 操做 JVMTI 的 Capability 參數;
  2. 使用系統參數;

動態庫被加載以後,虛擬機會先尋找一個 Agent 入口函數:

JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM *vm, char *options, void *reserved)
複製代碼

在這個Agent_OnLoad函數中,虛擬機傳入了三個參數:

  1. JavaVM *vmJVM上下文經過 JavaVM,能夠得到 JVMTI 的指針,並得到 JVMTI 函數的使用能力;
  2. char *options外部傳入的參數,好比上面例子中給的 opt1, opt2,它僅僅是一個字符串
  3. void *reserved:一個預留參數,沒必要關心它;

運行時載入,經過attach api,這是一套純Java的API,它負責動態地將dynamic module attach到指定進程id的Java進程內並觸發回調。例子以下:

import java.io.IOException;
import com.sun.tools.attach.VirtualMachine;

public class VMAttacher {
    public static void main(String[] args) throws Exception {
         // args[0]爲java進程id
         VirtualMachine virtualMachine = com.sun.tools.attach.VirtualMachine.attach(args[0]);
         // args[1]爲共享庫路徑,args[2]爲傳遞給agent的參數
         virtualMachine.loadAgentPath(args[1], args[2]);
         virtualMachine.detach();
    }
}
複製代碼

Attach API位於$JAVA_HOME/lib/tools.jar,因此在編譯時,須要將這個jar放入classpath。例如:

javac -cp $JAVA_HOME/lib/tools.jar VMAttacher.java pid /home/admin/agentlib/foo.so opt1,opt2
複製代碼

運行時載入,虛擬機會在運行時監聽並接受 Agent 的加載,在這個時候,它會使用 Agent 的:

JNIEXPORT jint JNICALL Agent_OnAttach(JavaVM* vm, char *options, void *reserved);
複製代碼

2.1.2 卸載

最後,Agent 完成任務,或者虛擬機關閉的時候,虛擬機都會調用一個相似於類析構函數的方法來完成最後的清理任務

JNIEXPORT void JNICALL Agent_OnUnload(JavaVM *vm)
複製代碼

2.2 JVMTI 環境和錯誤處理

使用 JVMTI 的過程,主要是 設置 JVMTI 環境,監聽虛擬機所產生的事件,以及在某些事件上加上回調函數

2.2.1 JVMTI 環境

經過操做 jvmtiCapabilities 來查詢、增長、修改 JVMTI 的環境參數。標準的 jvmtiCapabilities 定義了一系列虛擬機的功能,好比:

  1. can_redefine_any_class:定義了虛擬機是否支持重定義類;
  2. can_retransform_classes:定義了虛擬機是否支持運行的時候改變類定義;

若是熟悉 Java Instrumentation,必定不會對此感到陌生,由於 Instrumentation 就是對這些在 Java 層上的包裝。對用戶來講,只需得到 jvmtiCapabilities指針,就能夠查看當前 JVMTI 環境,瞭解虛擬機具備的一系列變量功能

// 取得 jvmtiCapabilities 指針
err = (*jvmti)->GetCapabilities(jvmti, &capa);
if (err == JVMTI_ERROR_NONE) {
    // 查看是否支持重定義類 
    if (capa.can_redefine_any_class) { ... }
} 
複製代碼

另外,虛擬機有本身的一些功能,一開始並未被啓動,那麼 增長或修改 jvmtiCapabilities 也是可能的,但不一樣的虛擬機對這個功能的處理也不太同樣,多數的虛擬機容許增改,可是有必定的限制,好比:僅支持在 Agent_OnLoad 時,即虛擬機啓動時做出,它某種程度上反映了虛擬機自己的構架。開發人員無須要考慮 Agent 的性能和內存佔用,就能夠在 Agent 被加載的時候啓用全部功能:

// 取得全部可用的功能
err = (*jvmti)->GetPotentialCapabilities(jvmti, &capa);
if (err == JVMTI_ERROR_NONE) { 
    err = (*jvmti)->AddCapabilities(jvmti, &capa); 
    ... 
}
複製代碼

最後要注意的,JVMTI 的函數調用都有其時間性,即特定的函數只能在特定的虛擬機狀態下才能調用

好比:SuspendThread(掛起線程)這個動做,僅在 Java 虛擬機處於運行狀態(live phase)才能調用,不然致使一個內部異常。

2.2.2 JVMTI 錯誤處理

JVMTI 沿用了基本的錯誤處理方式,即便用返回的錯誤代碼通知當前的錯誤,幾乎全部的 JVMTI 函數調用都具備如下模式:

jvmtiError err = jvmti->someJVMTImethod (somePara … );
複製代碼

其中 err 就是返回的錯誤代碼,不一樣函數的錯誤信息能夠在 Java 規範裏查到。

2.3 JVMTI 基本功能

JVMTI 的功能很是豐富,包含了 虛擬機中線程、內存 / 堆 / 棧,類 / 方法 / 變量,事件 / 定時器處理等等 20 多類功能,從功能上大體能夠分爲4類,以下:

  1. Heap:獲取全部類的信息,對象信息,對象引用關係,Full GC開始/結束,對象回收事件等;
  2. 線程與堆棧:獲取全部線程的信息,線程組信息,控制線程(start,suspend,resume,interrupt…), Thread Monitor(Lock),獲得線程堆棧,控制出棧,方法強制返回,方法棧本地變量等;
  3. Class & Object & Method & Field 元信息:class信息,符號表,方法表,redefine class(hotswap), retransform class,object信息,fields信息,method信息等;
  4. 工具類:線程cpu消耗,classloader路徑修改,系統屬性獲取等;

2.3.1 事件處理和回調函數

從上文咱們知道,使用 JVMTI 一個基本的方式就是設置回調函數,在某些事件發生的時候觸發並做出相應的動做

所以這一部分的功能很是基本,當前版本的 JVMTI 提供了許多事件(Event)的回調,包括 虛擬機初始化、開始運行、結束,類的加載,方法出入,線程始末等等。若是想對這些事件進行處理,須要首先爲該事件寫一個函數,而後在 jvmtiEventCallbacks 這個結構中指定相應的函數指針

好比:咱們對線程啓動感興趣,並寫了一個 HandleThreadStart 函數,那麼咱們須要在 Agent_OnLoad 函數里加入:

jvmtiEventCallbacks eventCallBacks; 
// 初始化
memset(&ecbs, 0, sizeof(ecbs));
// 設置函數指針
eventCallBacks.ThreadStart = &HandleThreadStart;
...
複製代碼

在設置了這些回調以後,就能夠調用下述方法,來最終完成設置。在接下來的虛擬機運行過程當中,一旦有線程開始運行發生,虛擬機就會回調 HandleThreadStart 方法。

jvmti->SetEventCallbacks(eventCallBacks, sizeof(eventCallBacks));
複製代碼

設置回調函數的時候,開發者須要注意如下幾點

  1. 如同 Java 異常機制同樣,若是在回調函數中本身拋出一個異常(Exception),或者在調用 JNI 函數的時候製造了一些麻煩,讓 JNI 丟出了一個異常,那麼 任何在回調以前發生的異常就會丟失,這就要求開發人員要在處理錯誤的時候須要小心
  2. 虛擬機不保證回調函數會被同步,換句話說,程序有可能同時運行同一個回調函數(好比,好幾個線程同時開始運行了,這個 HandleThreadStart 就會被同時調用幾回),那麼開發人員在開發回調函數時須要處理同步的問題

2.3.2 內存控制和對象獲取

內存控制是一切運行態的基本功能。 JVMTI 除了提供最簡單的內存申請和撤銷以外(這塊內存不受 Java 堆管理,開發人員須要自行進行清理工做,否則會形成內存泄漏),也提供了對 Java 堆的操做。

衆所周知,Java 堆中存儲了 Java 的類、對象和基本類型(Primitive),經過對堆的操做,開發人員能夠很容易的查找任意的類、對象,甚至能夠強行執行垃圾收集工做

JVMTI 中對 Java 堆的操做不同凡響,它沒有提供一個直接獲取的方式(因而可知,虛擬機對對象的管理並不是是哈希表,而是某種樹 / 圖方式),而是使用一個迭代器(iterater)的方式遍歷:

jvmtiError FollowReferences(jvmtiEnv* env, 
    jint heap_filter, 
    jclass klass, 
    jobject initial_object,// 該方式能夠指定根節點
    const jvmtiHeapCallbacks* callbacks,// 設置回調函數
    const void* user_data)
複製代碼

或者

jvmtiError IterateThroughHeap(jvmtiEnv* env, 
    jint heap_filter, 
    jclass klass, 
    const jvmtiHeapCallbacks* callbacks, 
    const void* user_data)// 遍歷整個 heap
複製代碼

在遍歷的過程當中,開發者能夠設定必定的條件,好比:指定是某一個類的對象,並設置一個回調函數,若是條件被知足,回調函數就會被執行。開發者能夠在回調函數中對當前傳回的指針進行打標記(tag)操做——這又是一個特殊之處,在第一遍遍歷中,只能對知足條件的對象進行 tag ;而後再使用 GetObjectsWithTags 函數,獲取須要的對象。

jvmtiError GetObjectsWithTags(jvmtiEnv* env, 
    jint tag_count, 
    const jlong* tags, // 設定特定的 tag,即咱們上面所設置的
    jint* count_ptr, 
    jobject** object_result_ptr, 
    jlong** tag_result_ptr)
複製代碼

若是你僅僅想對特定 Java 對象操做,應該避免設置其餘類型的回調函數,不然會影響效率,舉例來講,多增長一個 primitive 的回調函數,可能會使整個操做效率降低一個數量級。

2.3.3 線程和鎖

線程是 Java 運行態中很是重要的一個部分,在 JVMTI 中也提供了不少 API 進行相應的操做,包括查詢當前線程狀態,暫停,恢復或者終端線程,還能夠對線程鎖進行操做。

開發者能夠得到特定線程所擁有的鎖:

jvmtiError GetOwnedMonitorInfo(jvmtiEnv* env, 
    jthread thread, 
    jint* owned_monitor_count_ptr, 
    jobject** owned_monitors_ptr)
複製代碼

也能夠得到當前線程正在等待的鎖:

jvmtiError GetCurrentContendedMonitor(jvmtiEnv* env, 
    jthread thread, 
    jobject* monitor_ptr)
複製代碼

知道這些信息,事實上咱們也能夠設計本身的算法來判斷是否死鎖。更重要的是,JVMTI 提供了一系列的監視器(Monitor)操做,來幫助咱們在 native 環境中實現同步。

主要操做:構建監視器(CreateRawMonitor),獲取監視器(RawMonitorEnter),釋放監視器(RawMonitorExit),等待和喚醒監視器 (RawMonitorWait,RawMonitorNotify) 等操做,經過這些簡單鎖,程序的同步操做能夠獲得保證。

2.3.4 調試功能

調試功能是 JVMTI 的基本功能之一,這主要包括了設置斷點、調試(step)等,在 JVMTI 裏面,設置斷點的 API 自己很簡單:

jvmtiError SetBreakpoint(jvmtiEnv* env, 
    jmethodID method, 
    jlocation location)
複製代碼

jlocation 這個數據結構在這裏表明的是對應方法中一個可執行代碼的行數。在斷點發生的時候,虛擬機會觸發一個事件,開發者可使用在上文中介紹過的方式對事件進行處理。

2.3.5 JVMTI 數據結構

JVMTI 中使用的數據結構,首先也是一些標準的 JNI 數據結構,好比:jint,jlong ;其次,JVMTI 也定義了一些基本類型,好比:jthread,表示一個 thread,jvmtiEvent,表示 jvmti 所定義的事件;更復雜的有 JVMTI 的一些須要用結構體表示的數據結構,好比:堆的信息(jvmtiStackInfo)。

相關文章
相關標籤/搜索