日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > java >内容正文

java

Java调试那点事

發布時間:2025/4/5 java 24 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Java调试那点事 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

該文章來自于阿里巴巴技術協會(ATA)精選文章。

Java調試概述

程序猿都調式或者debug過Java代碼吧?都體會過被PM,PD,測試,業務同學們圍觀debug吧?說調試,先看看調試嚴格定義是什么。引用Wikipedia定義

調試(De-bug),又稱除錯,是發現和減少計算機程序或電子儀器設備中程序錯誤的一個過程。調試的基本步驟:
1. 發現程序錯誤的存在
2. 以隔離、消除的方式對錯誤進行定位
3. 確定錯誤產生的原因
4. 提出糾正錯誤的解決辦法
5. 對程序錯誤予以改正,重新測試

用調試的好處是我們就無需每次新測試都要重新編譯了,不用copy-paste一堆的System.out.println(很low但很多時候很管用有沒有?)。

更多時候我們調試最直接簡單的辦法就是IDE,Java程序員用的最多的必然是Eclipse,Netbeans和IntelliJ也有各自忠實的粉絲,各有優劣。關于用IDE如何調試可以另起一個話題再討論。

除了IDE之外,JDK也自帶了一些命令行調試工具也很方便。大家用的比較多的如下表所示:

命令描述
jdb命令行調試工具
jps列出所有Java進程的PID
jstack列出虛擬機進程的所有線程運行狀態
jmap列出堆內存上的對象狀態
jstat記錄虛擬機運行的狀態,監控性能
jconsole虛擬機性能/狀態檢查可視化工具


具體用法可以參考JDK文檔,這些大家在線上調試應用的時候用的也不少,比如一般線上load高的問題排查步驟是

  • 先用top找到耗資源的進程
  • ps+grep找到對應的java進程/線程
  • jstack分析哪些線程阻塞了,阻塞在哪里
  • jstat看看FullGC頻率
  • jmap看看有沒有內存泄露
  • 但這個也不是今天的重點,那么問題來了(blue fly is the strongest):這些工具如何能獲取遠程Java進程的信息的?又是如何遠程控制Java進程的運行的? 相信有不少人和我一樣對這些工具的 實現原理 很好奇,本文就嘗試介紹下各中緣由。

    Java調試體系JPDA簡介

    Java虛擬機設計了專門的API接口供調試和監控虛擬機使用,被稱為Java平臺調試體系即Java Platform Debugger Architecture(JPDA)。JPDA按照抽象層次,又分為三層,分別是

    • JVM TI - Java VM Tool Interface
      • 虛擬機對外暴露的接口,包括debug和profile
    • JDWP - Java Debug Wire Protocol
      • 調試器和應用之間通信的協議
    • JDI - Java Debug Interface
      • Java庫接口,實現了JDWP協議的客戶端,調試器可以用來和遠程被調試應用通信

    用一個不是特別準確但是比較容易理解的類比,大家可以和HTTP做比較,可以推斷他就是一個典型的C/S應用,所以也可以很自然的想到,JDI是用TCP Socket和虛擬機通信的,后面會詳細再介紹。

    • IDE+JDI = 瀏覽器
    • JDWP = HTTP
    • JVMTI = RESTful接口
    • Debugee虛擬機= REST服務端

    和其他的Java模塊一樣,Java只定義了Spec規范,也提供了參考實現(Reference Implementation),但是第三方完全可以參照這個規范,按照自己的需要去實現其中任意一個組件,原則上除了規范上沒有定義的功能,他們應該能正常的交互,比如Eclipse就沒有用Sun/Oracle的JDI,而是自己實現了一套(由于開源license的兼容原因),因為直接用JDWP協議調用JVMTI是不會受GPL“污染”的。的確有第三方調試工具基于JVMTI做了一套調試工具,這樣效率更高,功能更豐富,因為JDI出于遠程調用的安全考慮,做了一些功能的限制。用戶還可以不用JDI,用自己熟悉的C或者腳本語言開發客戶端,遠程調試Java虛擬機,所以JPDA真個架構是非常靈活的。

    JVMTI

    JVMTI是整個JPDA中最中要的API,也是虛擬機對外暴露的接口,掌握了JVMTI,你就可以真正完全掌控你的虛擬機,因為必須通過本地加載,所以暴露的豐富功能在安全上也沒有太大問題。更完整的API內容可以參考JVMTI SPEC:

    • 虛擬機信息
      • 堆上的對象
      • 線程和棧信息
      • 所有的類信息
      • 系統屬性,運行狀態
    • 調試行為
      • 設置斷點
      • 掛起現場
      • 調用方法
    • 事件通知
      • 斷點發生
      • 異步調用

    在JPDA的這個圖里,agent是其中很重要的一個模塊,正是他把JDI,JDWP,JVMTI三部分串聯成了一個整體。簡單來說agent的特性有

    • C/C++實現的
    • 被虛擬機以動態庫的方式加載
    • 能調用本地JVMTI提供的調試能力
    • 實現JDWP協議服務器端
    • 與JDI(作為客戶端)通信(socket/shmem等方式)

    Code speak louder than words. 上個代碼加注釋來解釋:

    // Agent_OnLoad必須是入口函數,類似于main函數,規范規定 JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM *vm, char *options, void *reserved) {....MethodTraceAgent* agent = new MethodTraceAgent();agent->Init(vm);agent->AddCapability();agent->RegisterEvent();... }/****** AddCapability(): init(): 初始化jvmti函數指針,所有功能的函數入口 *****/jvmtiEnv* MethodTraceAgent::m_jvmti = 0;jint ret = (vm)->GetEnv(reinterpret_cast<void**>(&jvmti), JVMTI_VERSION_1_0);/****** AddCability(): 確認agent能訪問的虛擬機接口 *****/jvmtiCapabilities caps;memset(&caps, 0, sizeof(caps));caps.can_generate_method_entry_events = 1;// 設置當前環境m_jvmti->AddCapabilities(&caps);/****** RegisterEvent(): 創建一個新的回調函數 *****/ jvmtiEventCallbacks callbacks;memset(&callbacks, 0, sizeof(callbacks));callbacks.MethodEntry = &MethodTraceAgent::HandleMethodEntry;// 設置回調函數m_jvmti->SetEventCallbacks(&callbacks, static_cast<jint>(sizeof(callbacks)));// 開啟事件監聽m_jvmti->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_METHOD_ENTRY, 0);/****** HandleMethodEntry: 注冊的回調,獲取對應的信息 *****/// 獲得方法對應的類m_jvmti->GetMethodDeclaringClass(method, &clazz);// 獲得類的簽名m_jvmti->GetClassSignature(clazz, &signature, 0);// 獲得方法名字m_jvmti->GetMethodName(method, &name, NULL, NULL);

    寫好agent后,需要編譯,并在啟動Java進程時指定加載路徑

    // 編譯動態鏈接庫 g++ -w -I${JAVA_HOME}/include/ -I${JAVA_HOME}/include/linux MethodTraceAgent.cpp Main.cpp -fPIC -shared -o libAgent.so// 拷貝到 LD_LIBRARY_PATH export LD_LIBRARY_PATH=/home/xiaoxia/lib cp libAgent.so ~/lib// 運行測試效果,記得load編譯的動態庫 javac MethodTraceTest.java java -agentlib:Agent=first MethodTraceTest



    Agent實現的動態鏈接庫其實有兩種加載方式:

    • 虛擬機啟動初期加載 這個鏈接庫必須實現Agent_OnLoad作為函數入口。這種方式可以利用的接口和功能更多,因為他在被調式虛擬機運行的應用初始化之前就被調用了,但是限制是必須以顯示的參數指定啟動方式,這在線上環境上是不大現實的。
    java -agentlib:<agent-lib-name>=<options> JavaClass//Linux從LD_LIBRARY_PATH找so文件, Windows從PATH找該DLL文件。java -agentpath:<path-to-agent>=<options> JavaClass//直接從絕對路徑查找
    • 動態加載 這是更靈活的方式,Java進程可以正常啟動,如果需要,通過Sun/Orale提供的私有Attach API可以連上對應的虛擬機,再通過JPDA方式控制,不過因為虛擬機已經開始運行了,所以功能上會有限制。我們比較熟悉的jstack等jdk工具就是通過這種方式做的,動態庫必須實現Agent_OnAttach作為函數入口。如果有興趣理解Attach機制細節的話,可以參考這個blog,簡單來說,就是虛擬機默認起了一個線程(沒錯,就是jstack時看到Signal Dispatcher這貨),專門接受處理進程間singal通知,當他收到SIGQUIT時,就會啟動一個新的socket監聽線程(就是jstack看到的Attach Listener線程)來接收命令,Attach Listener就是一個agent實現,他能處理很多dump命令,更重要的是他能再加載其他agent,比如jdwp agent。



    通過Attach機制,我們能自己非常方便的實現一個jinfo或者其他jdk tools,只需通過JPS獲取pid,在通過attach api去load我們提供的agent,完整的jinfo例子也在附件里。

    import java.io.IOException; import com.sun.tools.attach.VirtualMachine;public class JInfo {public static void main(String[] args) throws Exception {String pid = args[0]; String agentName = "JInfoAgent"; System.out.printf("Atach to Pid %s, dynamic load agent %s \n", pid, agentName);VirtualMachine virtualMachine = com.sun.tools.attach.VirtualMachine.attach(pid);virtualMachine.loadAgentLibrary(agentName, null);virtualMachine.detach();} }

    JDWP

    JDWP 是 Java Debug Wire Protocol 的縮寫,它定義了調試器(debugger)和被調試的 Java 虛擬機(debugee)之間的通信協議。他就是同過JVMTI Agent實現的,簡單來說,他就是對JVMTI調用(輸入和輸出,事件)的通信定義。

    JDWP 有兩種基本的包(packet)類型:命令包(command packet)和回復包(reply packet)。JDWP 本身是無狀態的,因此對 命令出現的順序并不受限制。而且,JDWP 可以是異步的,所以命令的發送方不需要等待接收到回復就可以繼續發送下一個命令。Debugger 和 Debugee 虛擬機都有可能發送命令:

    • Debugger 通過發送命令獲取Debugee虛擬機的信息以及控制程序的執行。Debugger虛擬機通過發送 命令通知 Debugger 某些事件的發生,如到達斷點或是產生異常。
    • 回復是用來確認對應的命令是否執行成功(在包定義有一個flag字段對應),如果成功,回復還有可能包含命令請求的數據,比如當前的線程信息或者變量的值。從 Debugee虛擬機發送的事件消息是不需要回復的。

    下圖展示了一個可能的實現方式,再次強調下,Java的世界里只定義了規范(Spec),很多實現細節可以自己提供,比如虛擬機就有很多中實現(Sun HotSpot,IBM J9,Google Davik)。

    一般我們啟動遠程調試時,都會看到如下參數,其實表面了JDWP Agent就是通過啟動一個socket監聽來接受JDWP命令和發送事件信息的,而且,這個TCP連接可以是雙向的:

    // debugge是server先啟動監聽,ide是client發起連接 agentlib:jdwp=transport=dt_socket,server=y,address=8000// debugger ide是server,通過JDI監聽,JDWP Agent作為客戶端發起連接 agentlib:jdwp=transport=dt_socket,address=myhost:8000

    JDI

    JDI屬于JPDA中最上層接口,也是Java程序員接觸的比較多的。他用起來也比較簡單,參考JDI的API Doc即可。所有的功能都和JVMTI提供的調試功能一一對應的(JVMTI還包括很多非調式接口,JDK5以前JVMTI是分為JVMDI和JVMPI的,分別對應調試debug和調優profile)。

    還是用一個例子來解釋最直接,大家可以看到基本的流程都是類似的,真個JPDA調試的核心就是通過JVMTI的?調用?和事件?兩個方向的溝通實現的。

    import java.util.List; import java.util.Map; import com.sun.jdi.*; import com.sun.jdi.connect.*; import com.sun.jdi.event.*; import com.sun.jdi.request.*;public class MethodTrace {private VirtualMachine vm;private Process process;private EventRequestManager eventRequestManager;private EventQueue eventQueue;private EventSet eventSet;private boolean vmExit = false;//write your own testclassprivate String className = "MethodTraceTest";public static void main(String[] args) throws Exception {MethodTrace trace = new MethodTrace();trace.launchDebugee();trace.registerEvent();trace.processDebuggeeVM();// Enter event looptrace.eventLoop();trace.destroyDebuggeeVM();}public void launchDebugee() {LaunchingConnector launchingConnector = Bootstrap.virtualMachineManager().defaultConnector();// Get arguments of the launching connectorMap<String, Connector.Argument> defaultArguments = launchingConnector.defaultArguments();Connector.Argument mainArg = defaultArguments.get("main");Connector.Argument suspendArg = defaultArguments.get("suspend");// Set class of main methodmainArg.setValue(className);suspendArg.setValue("true");try {vm = launchingConnector.launch(defaultArguments);} catch (Exception e) {// ignore}}public void processDebuggeeVM() {process = vm.process();}public void destroyDebuggeeVM() {process.destroy();}public void registerEvent() {// Register ClassPrepareRequesteventRequestManager = vm.eventRequestManager();MethodEntryRequest entryReq = eventRequestManager.createMethodEntryRequest();entryReq.setSuspendPolicy(EventRequest.SUSPEND_EVENT_THREAD);entryReq.addClassFilter(className);entryReq.enable();MethodExitRequest exitReq = eventRequestManager.createMethodExitRequest();exitReq.addClassFilter(className);exitReq.setSuspendPolicy(EventRequest.SUSPEND_EVENT_THREAD);exitReq.enable();}private void eventLoop() throws Exception {eventQueue = vm.eventQueue();while (true) {if (vmExit == true) {break;}eventSet = eventQueue.remove();EventIterator eventIterator = eventSet.eventIterator();while (eventIterator.hasNext()) {Event event = (Event) eventIterator.next();execute(event);if (!vmExit) {eventSet.resume();}}}}private void execute(Event event) throws Exception {if (event instanceof VMStartEvent) {System.out.println("VM started");} else if (event instanceof MethodEntryEvent) {Method method = ((MethodEntryEvent) event).method();System.out.printf("Enter -> Method: %s, Signature:%s\n",method.name(),method.signature());System.out.printf("\t ReturnType:%s\n", method.returnTypeName());} else if (event instanceof MethodExitEvent) {Method method = ((MethodExitEvent) event).method();System.out.printf("Exit -> method: %s\n",method.name());} else if (event instanceof VMDisconnectEvent) {vmExit = true;}} }

    總結

    整個JDPA有非常清晰的分層,各司其職,讓整個調式過程簡單可以擴展,而這一切其實都是構建在高司令巨牛逼的Java虛擬機抽象之上的,通過JVMTI將抽象良好的虛擬機控制暴露出來,讓開發者可以自由的掌控被調試的虛擬機。有興趣的同學可以運行下附近中的幾個例子,應該會有更充分的了解。

    而且由于規范的靈活性,如果有特殊需求,完全可以自己去重新實現和擴展,而且不限于Java,舉個例子,我們可以通過agent去加密解密加載的類,保護知識產權;我們可以記錄虛擬機運行過程,作為自動化測試用例;?我們還可以把線上問題的診斷實踐自動化下來,做一個快速預判?,爭取最寶貴的時間。

    總結

    以上是生活随笔為你收集整理的Java调试那点事的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。