1. 程式人生 > >Java Debug Interface(JDI)除錯多執行緒應用程式

Java Debug Interface(JDI)除錯多執行緒應用程式

        專案中遇到500多個執行緒併發執行,並將執行緒執行所生成的資料插入MySql資料庫,按設想,500個執行緒,資料庫中應有序號連續的500條記錄。然而,鬱悶的是資料庫中的記錄在第450條左右就開始不連續,部分記錄缺失。500多個執行緒幾乎是獨立的,它們之間存在的資源競爭已經做好同步了,因此,由於資源而阻塞的情況排除。再者,500個執行緒間優先順序均等同,我中間做了sleep操作,讓執行緒sleep時間與序號成正比例關係。按理說,每個執行緒都有機會執行,不存在有部分執行緒因未被排程到沒有執行的情況。

        針對上述問題,我先採用列印 Trace Log的方式來除錯。關於單步調式,線上程數目很多的情況下,或許不是最好的選擇。在 IDE 中通過新增斷點的方式除錯程式,往往會因為停在某一條執行緒的某個斷點上而錯失了其他執行緒的執行。另外,執行緒之間的排程往往無法預期,並且會因為斷點影響了實際的執行緒執行順序。

嘿嘿,允許我這麼說吧,對於多執行緒,尤其是很多個執行緒併發下的單步調式,我還真不很清楚快哭了,所以請專家見諒。日誌除錯的方式,可以幫助我們及時記錄每個執行緒的啟動、執行、想要watch的變數資訊等等,可以說好處多多。不太好的就是我們往往無法預期哪些關鍵點需要記錄,於是在整個程式的除錯過程中,需要不斷地加入 Log 呼叫,我已深有感受。。。這對於大尺寸的軟體開發專案無疑是噩夢,而且開發效率會受到很大的影響。因此,上網查詢多執行緒除錯工具,以希望解決糾結了我N就的問題。發現目前用的較多的貌似是JDI來開發的debugger吧,於是小試牛刀了下。

   一、認識JPDA和JDI

   JPD(

Java Platform Debugger Architecture)是一套架構,開發者可以通過這套架構來開發除錯用程式。目前這套架構被主流的 Java IDE(如 Eclipse、NetBeans 等)廣泛地採用。更多關於JPDA的詳細

介紹,可以參見JPDA官方文件以及“深入java調式體系”系列文章。Java Debuger Interface(JDI),定義了程式碼級別的除錯介面。目前,大多數的 JDI 實現都是通過 Java 語言編寫的。比如,大家再熟悉不過的 Eclipse IDE,它的除錯工具相信大家都使用過。它的兩個外掛 org.eclipse.jdt.debug.ui 和 org.eclipse.jdt.debug 與其強大的除錯功能密切相關,其中 org.eclipse.jdt.debug.ui 是 Eclipse 除錯工具介面的實現,而 org.eclipse.jdt.debug 則是 JDI 的一個完整實現。

     二、使用JDI開發調式工具

       (1)需求

               我們要開發的除錯工具大致滿足一下的通用需求:

1、 獨立於目標應用程式。

2、應該足夠簡單,並且能在通過少量的程式碼修改就能完成集中配置,這樣是幫助開發者不需要付出太多的努力就能開始除錯自己的多執行緒程式。

3、能夠抓取足夠的資訊,比如說異常的資訊,程式呼叫過程中的變數值等等。

4、所生成的 Log 應該足夠清晰,能夠按不同的執行緒來分離記錄,而不是按照時間的順序來生成每一條記錄,否則會給除錯帶來不便。

     (2)實現

      在IBM的技術網站上,提供了一個典型的基於JDI的除錯工具的示例,可參考http://www.ibm.com/developerworks/cn/java/j-lo-jdi/#download。該示例依據前面所提到的需求,用來 Profile 一個簡單的多執行緒程式的執行。它展示了執行緒執行棧快照、方法呼叫的入口引數值收集、異常過濾定製、類過濾配置、執行緒 Log 記錄等功能。相關的詳細資訊大家可以參考該網站上的內容。

     另外,需要把安裝路徑下的,如com.sun.java.jdk.win32.x86_1.6.0.013\lib下的tools.jar加入當前工程中,tools.jar中提供了com.sun.jdi的庫,開發時呼叫的介面大多都是這個包下的。

      總體來說,JDI的工作過程主要為以下操作:

  1. 繫結,分析工具和目標除錯程式的虛擬機器例項繫結;
  2. 事件註冊,分析工具向虛擬機器例項註冊相關事件請求,整個分析過程採取基於事件驅動的模式。
  3. 執行緒執行時資訊挖掘。
  4. 分類資訊生成。
       下面分析下每一操作的具體過程。

      繫結

     JDI 支援四種對目標程式的繫結方式,分別為:

  1. 分析器啟動目標程式虛擬機器例項
  2. 分析器繫結到已執行的目標程式虛擬機器例項
  3. 目標程式虛擬機器例項繫結到已執行的分析器
  4. 目標程式虛擬機器例項啟動分析器

     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;
			}
		}
		throw new Error("No launching connector");
	}

2、設定連線引數 

	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;
	}

3、啟動連線,獲取目標程式虛擬機器例項 

	VirtualMachine launchTarget(String mainArgs) {
		mainArgs = mainArgs.trim();
		LaunchingConnector connector = findLaunchingConnector();//獲取連線例項
		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、  註冊事件

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

  • EventRequest.SUSPEND_ALL : 事件發生時,掛起所有執行緒
  • EventRequest.SUSPEND_EVENT_THREAD : 事件發生時,掛起事件源執行緒
  • EventRequest.SUSPEND_NONE : 事件發生時,不掛起任何執行緒

    JDI 支援多種型別的 EventRequest,如 ExceptionRequest,MethodEntryRequest,MethodExitRequest,ThreadStartRequest 等,可以參考 文件

void setEventRequests() {
		EventRequestManager mgr = vm.eventRequestManager();
		// want all exceptions 註冊異常事件
		ExceptionRequest excReq = mgr.createExceptionRequest(null, true, true);
		// suspend so we can step
		excReq.setSuspendPolicy(EventRequest.SUSPEND_EVENT_THREAD);//事件發生時,掛起事件源執行緒
		excReq.enable();
               // 註冊進方法事件
		MethodEntryRequest menr = mgr.createMethodEntryRequest();
		for (int i = 0; i < excludes.length; ++i) {
			menr.addClassExclusionFilter(excludes[i]);
		}
		menr.setSuspendPolicy(EventRequest.SUSPEND_EVENT_THREAD);
		menr.enable();
		// 註冊出方法事件
		MethodExitRequest mexr = mgr.createMethodExitRequest();
		for (int i = 0; i < excludes.length; ++i) {
			mexr.addClassExclusionFilter(excludes[i]);
		}
		mexr.setSuspendPolicy(EventRequest.SUSPEND_EVENT_THREAD);
		mexr.enable();
		// 註冊執行緒啟動事件
		ThreadStartRequest tsr = mgr.createThreadStartRequest();
		// Make sure we sync on thread death
		tsr.setSuspendPolicy(EventRequest.SUSPEND_EVENT_THREAD);
		tsr.enable();
               // 註冊執行緒結束事件
		ThreadDeathRequest tdr = mgr.createThreadDeathRequest();
		// Make sure we sync on thread death
		tdr.setSuspendPolicy(EventRequest.SUSPEND_EVENT_THREAD);
		tdr.enable();
	}

2、分析器從事件佇列中獲取事件

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

	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 的除錯方式,它們的主要目的是向開發者提供以上兩方面資訊。本文分析器以單個執行緒為單位,來記錄執行緒執行資訊:

  1. 執行流程。分析器以方法作為最小顆粒度單位。分析器按照實際的執行緒執行順序記錄方法進出。
  2. 變數值。對於單個方法而言,其程式邏輯固定,方法的輸入值決定了方法內部執行流程。分析器將在方法入口和出口分別記錄該方法作用域內可見變數,便於開發者除錯。
  3. 執行棧資訊記錄。當異常發生時,執行棧中完好地儲存了呼叫幀資訊。分析器獲取執行緒棧中的所有幀,並記錄每個幀記錄的資訊,其中包含可見變數值、幀呼叫名稱等資訊。StackFrame 中變數資訊的獲取也是 JDI 所提供的特殊能力之一。關於幀棧(StackFrame)的詳情,請參考:sun的官網http://docs.oracle.com/javase/6/docs/jdk/api/jpda/jdi/com/sun/jdi/StackFrame.html執行流程。
執行緒執行流程

     執行緒執行流程可劃分:執行緒啟動→ run() →進入方法→ ... →退出方法→執行緒結束。通過向虛擬機器例項註冊 ThreadStartRequest,MethodEntryRequest,MethodExitRequest 和 ThreadDeathRequest 事件的方式記錄執行過程。

	// Forward event for thread specific processing
	private void methodEntryEvent(MethodEntryEvent event) {
		threadTrace(event.thread()).methodEntryEvent(event);
	}

	// Forward event for thread specific processing
	private void methodExitEvent(MethodExitEvent event) {
		threadTrace(event.thread()).methodExitEvent(event);
	}

	void threadDeathEvent(ThreadDeathEvent event) {
		ThreadTrace trace = (ThreadTrace) traceMap.get(event.thread());
		if (trace != null) { // only want threads we care about
			trace.threadDeathEvent(event); // Forward event
		}
	}
       //獲取執行流程1——執行緒啟動
	void threadStartEvent(ThreadStartEvent event) {
		threadTrace(event.thread()).threadStartEvent(event);
	}
可見變數資訊抓取 
以下程式碼中抓取的是name和iValue兩個變數。
		private void printVisiableVariables()
		{
			try
			{
				this.thread.suspend();
				if(this.thread.frameCount()>0)
				{
					//retrieve current method frame  獲取當前方法所在的幀
					StackFrame frame = this.thread.frame(0);
					
					Field field2=frame.thisObject().referenceType().fieldByName("name");
					increaseIndent();
					println(field2.name() + "\t"
							+ field2.typeName()
							+ "\t" + frame.thisObject().getValue(field2));
					decreaseIndent();
					Field field1=frame.thisObject().referenceType().fieldByName("iValue");
					increaseIndent();
					println(field1.name() + "\t"
							+ field1.typeName()
							+ "\t" + frame.thisObject().getValue(field1));
					decreaseIndent();
				}
			}
			catch(Exception e)
			{
				//ignore
			}
			finally
			{
				this.thread.resume();
			}
		}
異常時執行緒棧快照 
                //異常事件執行緒棧快照
		private void printStackSnapShot() {
			if (isMainThreadOrCreatedFromMain(this.thread)) {
				try {
					this.thread.suspend();
					println("Thread Status:" + this.thread.status());
					println("FrameCount in thread:" + this.thread.frameCount());
					//獲取執行緒棧
					List<StackFrame> frames = this.thread.frames();
					//獲取執行緒棧資訊
					for (StackFrame frame : frames) {
						println("Frame(" + frame.location()
								+ ")");
						if (frame.thisObject() != null) {
							increaseIndent();//獲取當前物件應該的所有欄位資訊
							println("");//獲取幀的可見變數資訊
							List<Field> fields = frame.thisObject()
									.referenceType().allFields();
							for (Field field : fields) {
								println(field.name() + "\t"
										+ field.typeName()
										+ "\t" 
										+ frame.thisObject().getValue(field));
							}
							decreaseIndent();
						}
						List<LocalVariable> lvs = frame.visibleVariables();
						increaseIndent();
						println("");
						for (LocalVariable lv : lvs) {
							println(lv.name() + "\t" 
									+ lv.typeName() + "\t" 
									+ frame.getValue(lv));
						}
						decreaseIndent();
					}
				} catch (Exception e) {
					// ignore the exception
				} finally {
					this.thread.resume();
				}
			}
		}
分類資訊生成log

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

  1. 分析器 Log 實現結構

    Trace 為分析器入口類,它負責建立繫結連線,生成目標程式虛擬機器例項;EventThread 負責從虛擬機器例項的事件佇列中獲取事件,交由對應的 ThreadTrace 處理,它同時維護著一張 ThreadReference 和 ThreadTrace 一一對應關係的對映表;ThreadTrace 負責分析 ThreadReference 資訊,並將結果記錄在 logRecord 的快取中,每個 ThreadTrace 實現了單個執行緒資訊的追蹤,詳見圖 1。

2、目標程式

     由兩個核心類組成:MainThread 和 CounterThread。MainThread 是程式的主類,它負責啟動兩個 CounterThread 執行緒例項並丟擲兩類異常:使用者自定義異常 UserDefinedException 和執行時異常 NullPointerException;CounterThread 是一個簡單的計數執行緒。整個目標程式模擬的是多執行緒和異常的環境。

3、分析結果

   Log 依照目標程式的呼叫層次進行縮排,清晰地展現每個執行緒的執行邏輯和變數資訊,詳見如下圖。


結語

 JDI確實在多執行緒除錯中起到了作用。問題是專案程式程式碼量大,設定了很多過濾,即ExcludeClass,還是沒法用JDI。。。未完待續吧。

參考: