Java 動態調試技術原理及實踐
- 2019 年 11 月 10 日
- 筆記
調試是發現和減少電腦程式或電子儀器設備中程式錯誤的一個過程。最常用的斷點調試技術會在斷點位置停頓,導致應用停止響應。本文將介紹一種Java動態調試技術,希望能對大家有幫助。同時也歡迎讀者朋友們一起交流,繼續探索動態化調試技術。
1. 動態調試要解決的問題
斷點調試是我們最常使用的調試手段,它可以獲取到方法執行過程中的變數資訊,並可以觀察到方法的執行路徑。但斷點調試會在斷點位置停頓,使得整個應用停止響應。在線上停頓應用是致命的,動態調試技術給了我們創造新的調試模式的想像空間。本文將研究Java語言中的動態調試技術,首先概括Java動態調試所涉及的技術基礎,接著介紹我們在Java動態調試領域的思考及實踐,通過結合實際業務場景,設計並實現了一種具備動態性的斷點調試工具Java-debug-tool,顯著提高了故障排查效率。
2. Java Agent技術
JVMTI (JVM Tool Interface)是Java虛擬機對外提供的Native編程介面,通過JVMTI,外部進程可以獲取到運行時JVM的諸多資訊,比如執行緒、GC等。Agent是一個運行在目標JVM的特定程式,它的職責是負責從目標JVM中獲取數據,然後將數據傳遞給外部進程。載入Agent的時機可以是目標JVM啟動之時,也可以是在目標JVM運行時進行載入,而在目標JVM運行時進行Agent載入具備動態性,對於時機未知的Debug場景來說非常實用。下面將詳細分析Java Agent技術的實現細節。
2.1 Agent的實現模式
JVMTI是一套Native介面,在Java SE 5之前,要實現一個Agent只能通過編寫Native程式碼來實現。從Java SE 5開始,可以使用Java的Instrumentation介面(java.lang.instrument)來編寫Agent。無論是通過Native的方式還是通過Java Instrumentation介面的方式來編寫Agent,它們的工作都是藉助JVMTI來進行完成,下面介紹通過Java Instrumentation介面編寫Agent的方法。
2.1.1 通過Java Instrumentation API
- 實現Agent啟動方法
Java Agent支援目標JVM啟動時載入,也支援在目標JVM運行時載入,這兩種不同的載入模式會使用不同的入口函數,如果需要在目標JVM啟動的同時載入Agent,那麼可以選擇實現下面的方法:
[1] public static void premain(String agentArgs, Instrumentation inst); [2] public static void premain(String agentArgs);
JVM將首先尋找[1],如果沒有發現[1],再尋找[2]。如果希望在目標JVM運行時載入Agent,則需要實現下面的方法:
[1] public static void agentmain(String agentArgs, Instrumentation inst); [2] public static void agentmain(String agentArgs);
這兩組方法的第一個參數AgentArgs是隨同 「– javaagent」一起傳入的程式參數,如果這個字元串代表了多個參數,就需要自己解析這些參數。inst是Instrumentation類型的對象,是JVM自動傳入的,我們可以拿這個參數進行類增強等操作。
- 指定Main-Class
Agent需要打包成一個jar包,在ManiFest屬性中指定「Premain-Class」或者「Agent-Class」:
Premain-Class: class Agent-Class: class
- 掛載到目標JVM
將編寫的Agent打成jar包後,就可以掛載到目標JVM上去了。如果選擇在目標JVM啟動時載入Agent,則可以使用 "-javaagent:<jarpath>[=<option>]",具體的使用方法可以使用「Java -Help」來查看。如果想要在運行時掛載Agent到目標JVM,就需要做一些額外的開發了。
com.sun.tools.attach.VirtualMachine 這個類代表一個JVM抽象,可以通過這個類找到目標JVM,並且將Agent掛載到目標JVM上。下面是使用com.sun.tools.attach.VirtualMachine進行動態掛載Agent的一般實現:
private void attachAgentToTargetJVM() throws Exception { List<VirtualMachineDescriptor> virtualMachineDescriptors = VirtualMachine.list(); VirtualMachineDescriptor targetVM = null; for (VirtualMachineDescriptor descriptor : virtualMachineDescriptors) { if (descriptor.id().equals(configure.getPid())) { targetVM = descriptor; break; } } if (targetVM == null) { throw new IllegalArgumentException("could not find the target jvm by process id:" + configure.getPid()); } VirtualMachine virtualMachine = null; try { virtualMachine = VirtualMachine.attach(targetVM); virtualMachine.loadAgent("{agent}", "{params}"); } catch (Exception e) { if (virtualMachine != null) { virtualMachine.detach(); } } }
首先通過指定的進程ID找到目標JVM,然後通過Attach掛載到目標JVM上,執行載入Agent操作。VirtualMachine的Attach方法就是用來將Agent掛載到目標JVM上去的,而Detach則是將Agent從目標JVM卸載。關於Agent是如何掛載到目標JVM上的具體技術細節,將在下文中進行分析。
2.2 啟動時載入Agent
2.2.1 參數解析
創建JVM時,JVM會進行參數解析,即解析那些用來配置JVM啟動的參數,比如堆大小、GC等;本文主要關註解析的參數為-agentlib、 -agentpath、 -javaagent,這幾個參數用來指定Agent,JVM會根據這幾個參數載入Agent。下面來分析一下JVM是如何解析這幾個參數的。
// -agentlib and -agentpath if (match_option(option, "-agentlib:", &tail) || (is_absolute_path = match_option(option, "-agentpath:", &tail))) { if(tail != NULL) { const char* pos = strchr(tail, '='); size_t len = (pos == NULL) ? strlen(tail) : pos - tail; char* name = strncpy(NEW_C_HEAP_ARRAY(char, len + 1, mtArguments), tail, len); name[len] = '