程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> 使用Java Debug Interface(JDI)調試多線程應用程序

使用Java Debug Interface(JDI)調試多線程應用程序

編輯:關於JAVA

多線程環境下的程序調試是讓開發者頭痛的問題。在 IDE 中通過添加斷點的 方式調試程序,往往會因為停在某一條線程的某個斷點上而錯失了其他線程的執 行,線程之間的調度往往無法預期,並且會因為斷點影響了實際的線程執行順序 。因此,在調試多線程程序時,開發者往往會選擇打印 Trace Log 的方式來幫 助調試。

使用 Log 來幫助調試的問題在於,開發者往往無法預期哪些關鍵點需要記錄 ,於是在整個程序的調試過程中,需要不斷的加入 Log 調用,編譯生成可執行 程序並部署,這對於大尺寸的軟件開發項目無疑是噩夢,會直接影響到開發效率 。

有沒有一種辦法,可以獨立於程序代碼,能在運行期間綁定到程序上並獲取 程序運行過程當中的關鍵信息呢?更重要的,這種方法應該是可定制的,開發者 可以通過少量的努力,就可以達到特定的調試目的。答案是肯定的。通過使用 java Debug Interface(JDI),開發者可以快速開發定制出適用於自己的線程 Profiling 工具。這樣的工具獨立於主程序,並且可高度定制。在接下來的文章 中,我們將介紹如何實現該工具。

認識 JPDA 和 JDI

從 J2SE 1.3 開始,Java 開始提供了一套叫做 Java Platform Debugger Architecture(JPDA)的架構,開發者可以通過這套架構來開發調試用程序。這 套架構被主流的 Java IDE(如 Eclipse、NetBeans 等)廣泛地采用。

具體來說,JPDA 不僅僅是一套 API 的組合,也不只是一個具體的工具。這 套架構提供了從目標程序、調試雙方的信息協議,到供開發者使用的結構調用, 都一一做出了定義。在 J2SE 5.0 中,它由三個部分組成:

Java Virtual Machine Tools Interface(JVMTI),是一套低級別的 native 接口。它定義了 Java 虛擬機所必需為調試提供的服務接口。JVMTI 在 Java 5.0 之前的前身是 JVMDI(Jave Virtual Machine Debug Interface)。

Java Debug Wire Protocol(JDWP),定義了調試雙方信息和請求的文本格 式。

Java Debuger Interface(JDI),定義了代碼級別的調試接口。

從開發者的角度來看,調試工具的開發既可以基於 JVMTI 也可以基於 JDI。 JVMTI 是 native 接口,使用起來相對復雜,並且需要 C 語言的基礎,因此, 在本文中,我們將介紹如何使用 JDI 這種最上層的方式來開發 Java 調試程序 。

需求分析

在接下的部分,我們將介紹如何使用 JDI 來開發一個用來調試多線程程序的 工具。在開始前,讓我們先列出這個工具需要滿足的功能:

獨立於目標應用程序的。

應該足夠簡單,並且能在通過少量的代碼修改就能完成集中配置,這樣是幫 助開發者不需要付出太多的努力就能開始調試自己的多線程程序。

能夠抓取足夠的信息,比如說異常的信息,程序調用過程中的變量值等等。

所生成的 Log 應該足夠清晰,能夠按不同的線程來分離記錄,而不是按照時 間的順序來生成每一條記錄,否則會給調試帶來不便。

實現

在文章最後的 示例代碼 中,我們展示了一個典型的基於 JDI 的調試工具邏 輯,並且用它來 Profile 一個簡單的多線程程序的執行。根據前面所提到的需 求,代碼展示了線程運行棧快照、方法調用的入口參數值收集、異常過濾定制、 類過濾配置、線程 Log 記錄等功能。具體來說:

獨立於目標程序

分析工具可以通過如下方式啟動:

java Trace options class args

支持的 options 參數:

-output 文件名:工具生成的 Log 的路徑

class 是目標程序的入口類,args 為目標程序的輸入參數

簡潔配置

異常過濾配置:

您可以在 ExceptionConfig.properties 屬性文件中配置所需記錄異常類型 。在 Demo 代碼中配置了對於 NullPointerException 和 UserDefinedException 兩種異常,分析工具將追蹤這兩種異常情況。

ExceptionName = exceptions.UserDefinedException;java.lang.NullPointerException

類過濾配置:

您可以在 ClassExcludeConfig.properties 屬性文件中配置被過濾的類模式 ,分析工具將不會處理被過濾類的任何事件。

ExcludedClassPattern=java.*;javax.*;sun.*;com.sun.*;com.ibm.*

運行

在目標的主程序的生命周期中,分析器完成以下操作:

綁定,分析工具和目標調試程序的虛擬機實例綁定;

事件注冊,分析工具向虛擬機實例注冊相關事件請求,整個分析過程采取基 於事件驅動的模式。

線程運行時信息挖掘。

分類信息生成。

以上四點操作滿足了需求:通過采用綁定機制實現調試程序和工具程序的獨 立,分析工具和目標程序以監聽端口、共享內存等方式進行通信,無須目標程序 進行任何代碼修改即可實現調試。采用基於事件的機制可以幫助開發者依據實際 需要集中注冊和處理事件。作為基礎框架,分析工具注冊了支持異常、執行流程 等事件,並提供了異常時運行棧快照,方法進出參數記錄等功能實現信息抓取。 支持單線程為單位的 Log 記錄,將開發者從無序不可預測的多線程執行中擺脫 出來,對調試程序提供幫助。

下面將詳細闡述實現步驟:

綁定

JDI 支持四種對目標程序的綁定方式,分別為:

分析器啟動目標程序虛擬機實例

分析器綁定到已運行的目標程序虛擬機實例

目標程序虛擬機實例綁定到已運行的分析器

目標程序虛擬機實例啟動分析器

JDI 支持一個分析器綁定多個目標程序,但一個目標程序只能綁定一個分析 器。為支持以上綁定,JDI 對應有 LaunchingConnector,AttachingConnector 和 ListeningConnector,具體類介紹可以參照 文檔。

本文采用第一種綁定方式闡述如何開發定制的多線程分析器,其它綁定方式 可以參照 文檔。

綁定過程分為三個步驟:

獲取連接實例

清單 1. 獲取連接實例

LaunchingConnector findLaunchingConnector() {
   List connectors = Bootstrap.virtualMachineManager ().allConnectors();
   Iterator iter = connectors.iterator();
   while (iter.hasNext()) {
    Connector connector = (Connector) iter.next();
    if ("com.sun.jdi.CommandLineLaunch".equals(connector.name ())) {
       return (LaunchingConnector) connector;
     }
   }
}

Bootstrap.virtualMachineManager().allConnectors() 返回所有已知的 Connector 對象實例。選擇返回 com.sun.jdi.CommandLineLaunch 連接實例, 表示使用第一種綁定方式。

設置連接參數

清單 2. 設置連接參數

/**參數: 
* connector為清單1.中獲取的Connector連接實例
* mainArgs為目標程序main函數所在的類
**/
Map connectorArguments(LaunchingConnector connector, String  mainArgs) {
   Map arguments = connector.defaultArguments();
   Connector.Argument mainArg = (Connector.Argument)  arguments.get("main");
   if (mainArg == null) {
     throw new Error("Bad launching connector");
   }
   mainArg.setValue(mainArgs);
   return arguments;
}

每個連接實例都有對應的默認參數,啟動連接之前需要設置必須的參數,對 於 CommandLineLaunch 連接實例需要設置主程序啟動目標程序虛擬機實例所需 的參數。

啟動連接,獲取目標程序虛擬機實例

清單 3. 啟動連接

/**參數: 
* mainArgs為目標程序main函數所在的類
**/
VirtualMachine launchTarget(String mainArgs) {
   //findLaunchingConnector:獲取連接
   LaunchingConnector connector = findLaunchingConnector();
   //connectorArguments:設置連接參數
   Map arguments = connectorArguments(connector,  mainArgs);
   try {
     return connector.launch(arguments);//啟動連接
   } catch (IOException exc) {
     throw new Error("Unable to launch target VM: " +  exc);
   } catch (IllegalConnectorArgumentsException exc) {
     throw new Error("Internal error: " + exc);
   } catch (VMStartException exc) {
     throw new Error("Target VM failed to initialize: "  + exc.getMessage());
   }
}

清單 1 和清單 2 分別獲取連接實例和啟動所需的變量,通過調用 connector.launch(arguments) 啟動連接,實現了分析器和目標程序的綁定。

注冊事件

分析器和目標程序之間采用基於事件的模式進行通信。分析器向虛擬機實例 注冊所關注的事件。事件發生時,虛擬機將相關事件信息放入事件隊列中,采用 生產者 - 消費者 的模式與分析器同步。

注冊事件

EventRequestManager 管理事件請求,它支持創建、刪除和查詢事件請求。 EventRequest 支持三種掛起策略:

EventRequest.SUSPEND_ALL : 事件發生時,掛起所有線程

EventRequest.SUSPEND_EVENT_THREAD : 事件發生時,掛起事件源線程

EventRequest.SUSPEND_NONE : 事件發生時,不掛起任何線程

JDI 支持多種類型的 EventRequest,如 ExceptionRequest, MethodEntryRequest,MethodExitRequest,ThreadStartRequest 等,可以參考 文檔。

清單 4. 注冊事件

EventRequestManager mgr = vm.eventRequestManager();
// 注冊異常事件
ExceptionRequest excReq = mgr.createExceptionRequest(null, true,  true);
excReq.setSuspendPolicy(EventRequest.SUSPEND_EVENT_THREAD);
excReq.enable();
// 注冊進方法事件
MethodEntryRequest menr = mgr.createMethodEntryRequest();
menr.setSuspendPolicy(EventRequest.SUSPEND_NONE);
menr.enable();
// 注冊出方法事件
MethodExitRequest mexr = mgr.createMethodExitRequest();
mexr.setSuspendPolicy(EventRequest.SUSPEND_NONE);
mexr.enable();
// 注冊線程啟動事件
ThreadStartRequest tsr = mgr.createThreadStartRequest();
tsr.setSuspendPolicy(EventRequest.SUSPEND_EVENT_THREAD);
tsr.enable();
// 注冊線程結束事件
ThreadDeathRequest tdr = mgr.createThreadDeathRequest();
tdr.setSuspendPolicy(EventRequest.SUSPEND_EVENT_THREAD);
tdr.enable();

分析器從事件隊列中獲取事件

EventQueue 用來管理目標虛擬機實例的事件,事件會被加入 EventQueue 中 。分析器調用 EventQueue.remove(),如果事件隊列中存在事件,則返回不可修 改的 EventSet 實例,否則分析器會被掛起直到有新的事件發生。處理完 EventSet 中的事件後,調用其 resume() 方法喚醒 EventSet 中所有事件發生 時可能掛起的線程。

清單 5. 獲取事件

public void run() {
   EventQueue queue = vm.eventQueue();
   while (connected) {
     try {
       EventSet eventSet = queue.remove();
       EventIterator it = eventSet.eventIterator();
       while (it.hasNext()) {
         handleEvent(it.nextEvent());
       }
       eventSet.resume();
     } catch (InterruptedException exc) {// Ignore
     } catch (VMDisconnectedException discExc) {
       handleDisconnectedException();
       break;
     }
   }
}

獲取多線程執行信息

執行流程和變量信息是調試程序最重要的兩方面。無論是通過 IDE 設置斷點 的調試方式,還是通過在程序中記 Log 的調試方式,它們的主要目的是向開發 者提供以上兩方面信息。本文分析器以單個線程為單位,來記錄線程運行信息:

執行流程。分析器以方法作為最小顆粒度單位。分析器按照實際的線程執行 順序記錄方法進出。

變量值。對於單個方法而言,其程序邏輯固定,方法的輸入值決定了方法內 部執行流程。分析器將在方法入口和出口分別記錄該方法作用域內可見變量,便 於開發者調試。

執行棧信息記錄。當異常發生時,執行棧中完好地保存了調用幀信息。分析 器獲取線程棧中的所有幀,並記錄每個幀記錄的信息,其中包含可見變量值、幀 調用名稱等信息。StackFrame 中變量信息的獲取也是 JDI 所提供的特殊能力之 一。

與 IDE 設置斷點的方法相比,提供的數據信息量相當,但分析器提供執行流 程信息更加的清晰;與在程序中記錄 Log 的方式相比,分析器在執行流程和信 息量兩方面都勝出。

以下將詳細介紹上面三方面信息抓取:

線程執行流程

線程執行流程可劃分:線程啟動→ run() →進入方法→ ... →退出方法→ 線程結束。通過向虛擬機實例注冊 ThreadStartRequest,MethodEntryRequest ,MethodExitRequest 和 ThreadDeathRequest 事件的方式記錄執行過程。事件 注冊詳細見清單 4,清單 6 列出分析器對於以上事件的處理方法。

清單 6. 獲取執行流程

void threadStartEvent(ThreadStartEvent event) {
   println("Thread " + event.thread().name() + "  Start");
}
void methodEntryEvent(MethodEntryEvent event) {
   println("Enter Method:" + event.method().name() + " --  "
     + event.method().declaringType().name());
   // 進入方法記錄可見變量值 
   this.printVisiableVariables();
}
void methodExitEvent(MethodExitEvent event) {
   println("Exit Method:" + event.method().name() + " --  "
     + event.method().declaringType().name());
   // 退出方法記錄可見變量值 
   this.printVisiableVariables();
}
void threadDeathEvent(ThreadDeathEvent event) {
   println("Thread " + event.thread().name() + " Dead");
}

可見變量信息抓取

清單 7. 可見變量信息抓取

private void printVisiableVariables()
{
   try{
     this.thread.suspend();
     if(this.thread.frameCount()>0) {
       //獲取當前方法所在的幀 
       StackFrame frame = this.thread.frame(0);
       List<LocalVariable> lvs =  frame.visibleVariables();
       for (LocalVariable lv : lvs) {
         println("Name:" + lv.name() + "\t" +  "Type:"
           + lv.typeName() + "\t" + "Value:"
           + frame.getValue(lv));
       }

     }
   } catch(Exception e){//ignore}
   finally{this.thread.resume();}
}

通過 this.thread.frame(0) 獲取當前方法對應的幀,調用 frame.visibleVariables() 取出當前方法幀的所有可見變量。

異常時線程棧快照

清單 8. 異常事件線程棧快照

private void printStackSnapShot() {
   try {
     this.thread.suspend();
     //獲取線程棧 
     List<StackFrame> frames = this.thread.frames ();
     //獲取線程棧信息
     for (StackFrame frame : frames) {
       if (frame.thisObject() != null) {
         //獲取當前對象應該的所有字段信息
         List<Field> fields = frame.thisObject ().referenceType().allFields();
         for (Field field : fields) {
           println(field.name() + "\t" +  field.typeName()+ "\t"
             + frame.thisObject().getValue(field));
         }
       }
       //獲取幀的可見變量信息
       List<LocalVariable> lvs =  frame.visibleVariables();
       for (LocalVariable lv : lvs) {
         println(lv.name() + "\t" + lv.typeName() +  "\t"
           + frame.getValue(lv));
       }
     }
   } catch (Exception e) {}
   finally { this.thread.resume();}
}

通過 this.thread.frames() 獲取異常發生時線程棧中所有幀信息,調用 frame.thisObject() 獲取對 this 指針的引用,進而獲取對象字段信息;對於 幀信息的抓取與清單 7 類似。

分類信息生成 Log

以單線程為記錄單元是分析器的特點,下面將從分析器 Log 實現結構、目標 程序所模擬的場景及分析結果三方面對示例代碼進行介紹。

分析器 Log 實現結構

Trace 為分析器入口類,它負責創建綁定連接,生成目標程序虛擬機實例; EventThread 負責從虛擬機實例的事件隊列中獲取事件,交由對應的 ThreadTrace 處理,它同時維護著一張 ThreadReference 和 ThreadTrace 一一 對應關系的映射表;ThreadTrace 負責分析 ThreadReference 信息,並將結果 記錄在 logRecord 的緩存中,每個 ThreadTrace 實現了單個線程信息的追蹤, 詳見圖 1。

圖 1. 分析器類圖 :

目標程序

目標程序由兩個核心類組成:MainThread 和 CounterThread。MainThread 是程序的主類,它負責啟動兩個 CounterThread 線程實例並拋出兩類異常:用 戶自定義異常 UserDefinedException 和運行時異常 NullPointerException; CounterThread 是一個簡單的計數線程。整個目標程序模擬的是多線程和異常的 環境。

分析結果

Log 依照目標程序的調用層次進行縮進,清晰地展現每個線程的執行邏輯和 變量信息,詳見清單 9。為了方便理解,我們在 log 中加入了注釋。

清單 9. Log

-- VM Started --
   ====== main ======
     Enter Method:main//
       Enter Method:<init>//MainThread 構造函數
         a   int   0
         b   int   0
         c   int   0
       Exit Method:<init>
       Enter  Method:makeABusinessException//makeABusinessException 方法調用
         a   int   0
         b   int   1
         c   int   2
         Enter Method:<init>//UserDefinedException  構造函數
           ...
         Exit Method:<init>
  //UserDefinedException 異常發生,抓取線程棧中所有幀信息
         exceptions.UserDefinedException(id=62) catch:  MainThread:30
           Frame(MainThread:44)
             a   int   0
             b   int   1
             c   int   2
             i   int   0
             d   int   4
           Frame(MainThread:23)
               e   int   4
             g   int   5
             mt   MainThread   instance of  MainThread(id=59)
             i   int   0
          // NullPointerException 異常發生,抓取線程棧信 息
       java.lang.NullPointerException(id=70) catch:  MainThread:30
               ...
  // 以下是兩個 CounterThread 線程的構造
       Enter Method:<init>
         name   java.lang.String   null
         index   int   0
       Exit Method:<init>
       Enter Method:<init>
         name   java.lang.String   null
         index   int   0
       Exit Method:<init>
     Exit Method:main
   ====== main end ======
   ====== Thread-1 ======
     Enter Method:run//run 方法調用
       name   java.lang.String   "thread1"
       index   int   0
  // 以下是 3 次 updateIndex 方法調用
       Enter Method:updateIndex
         name   java.lang.String   "thread1"
         index   int   0
       Exit Method:updateIndex
       Enter Method:updateIndex
         name   java.lang.String   "thread1"
         index   int   2
       Exit Method:updateIndex
       Enter Method:updateIndex
         name   java.lang.String   "thread1"
         index   int   4
       Exit Method:updateIndex
     Exit Method:run
   ====== Thread-1 end ======
   ====== Thread-2 ======
      Enter Method:run//run 方法調用
       name   java.lang.String   "thread2"
       index   int   0
  // 以下是 3 次 updateIndex 方法調用
       Enter Method:updateIndex
         name   java.lang.String   "thread2"
         index   int   1
       Exit Method:updateIndex
       Enter Method:updateIndex
         name   java.lang.String   "thread2"
         index   int   3
       Exit Method:updateIndex
       Enter Method:updateIndex
         name   java.lang.String   "thread2"
         index   int   5
       Exit Method:updateIndex
     Exit Method:run
   ====== Thread-2 end ======

結語

當開發多線程程序時,至少有兩個理由讓你選擇 JDI 來協助調試:

線程執行的時序變得越來越不可預測,在 IDE 中通過添加斷點來調試的方法 已經不能正確地反映程序運行狀況。

程序規模大,每一次 trace 語句的添加都會造成程序的再編譯,而這樣的編 譯需要花上很多時間。

因此,使用 JDI 開發自己的調試程序,有時會為開發者節省更多的時間。通 過本文的介紹和示例代碼的解讀,讀者可以著手開發自己的多線程調試程序了。

本文配套源碼

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved