99精品伊人亚洲|最近国产中文炮友|九草在线视频支援|AV网站大全最新|美女黄片免费观看|国产精品资源视频|精彩无码视频一区|91大神在线后入|伊人终合在线播放|久草综合久久中文

0
  • 聊天消息
  • 系統(tǒng)消息
  • 評論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

一文詳解Java動態(tài)調(diào)試技術(shù)

h1654155282.3538 ? 來源:美團(tuán)技術(shù)團(tuán)隊 ? 作者:美團(tuán)技術(shù)團(tuán)隊 ? 2020-10-18 11:33 ? 次閱讀
加入交流群
微信小助手二維碼

掃碼添加小助手

加入工程師交流群

調(diào)試是發(fā)現(xiàn)和減少計算機程序或電子儀器設(shè)備中程序錯誤的一個過程。最常用的斷點調(diào)試技術(shù)會在斷點位置停頓,導(dǎo)致應(yīng)用停止響應(yīng)。本文將介紹一種Java動態(tài)調(diào)試技術(shù),希望能對大家有幫助。

1. 動態(tài)調(diào)試要解決的問題

斷點調(diào)試是我們最常使用的調(diào)試手段,它可以獲取到方法執(zhí)行過程中的變量信息,并可以觀察到方法的執(zhí)行路徑。但斷點調(diào)試會在斷點位置停頓,使得整個應(yīng)用停止響應(yīng)。在線上停頓應(yīng)用是致命的,動態(tài)調(diào)試技術(shù)給了我們創(chuàng)造新的調(diào)試模式的想象空間。本文將研究Java語言中的動態(tài)調(diào)試技術(shù),首先概括Java動態(tài)調(diào)試所涉及的技術(shù)基礎(chǔ),接著介紹我們在Java動態(tài)調(diào)試領(lǐng)域的思考及實踐,通過結(jié)合實際業(yè)務(wù)場景,設(shè)計并實現(xiàn)了一種具備動態(tài)性的斷點調(diào)試工具Java-debug-tool,顯著提高了故障排查效率。

2. Java Agent技術(shù)

JVMTI (JVM Tool Interface)是Java虛擬機對外提供的Native編程接口,通過JVMTI,外部進(jìn)程可以獲取到運行時JVM的諸多信息,比如線程、GC等。Agent是一個運行在目標(biāo)JVM的特定程序,它的職責(zé)是負(fù)責(zé)從目標(biāo)JVM中獲取數(shù)據(jù),然后將數(shù)據(jù)傳遞給外部進(jìn)程。加載Agent的時機可以是目標(biāo)JVM啟動之時,也可以是在目標(biāo)JVM運行時進(jìn)行加載,而在目標(biāo)JVM運行時進(jìn)行Agent加載具備動態(tài)性,對于時機未知的Debug場景來說非常實用。下面將詳細(xì)分析Java Agent技術(shù)的實現(xiàn)細(xì)節(jié)。

2.1 Agent的實現(xiàn)模式

JVMTI是一套Native接口,在Java SE 5之前,要實現(xiàn)一個Agent只能通過編寫Native代碼來實現(xiàn)。從Java SE 5開始,可以使用Java的Instrumentation接口(java.lang.instrument)來編寫Agent。無論是通過Native的方式還是通過Java Instrumentation接口的方式來編寫Agent,它們的工作都是借助JVMTI來進(jìn)行完成,下面介紹通過Java Instrumentation接口編寫Agent的方法。

2.1.1 通過Java Instrumentation API

實現(xiàn)Agent啟動方法

Java Agent支持目標(biāo)JVM啟動時加載,也支持在目標(biāo)JVM運行時加載,這兩種不同的加載模式會使用不同的入口函數(shù),如果需要在目標(biāo)JVM啟動的同時加載Agent,那么可以選擇實現(xiàn)下面的方法:

JVM將首先尋找[1],如果沒有發(fā)現(xiàn)[1],再尋找[2]。如果希望在目標(biāo)JVM運行時加載Agent,則需要實現(xiàn)下面的方法:

這兩組方法的第一個參數(shù)AgentArgs是隨同 “– javaagent”一起傳入的程序參數(shù),如果這個字符串代表了多個參數(shù),就需要自己解析這些參數(shù)。inst是Instrumentation類型的對象,是JVM自動傳入的,我們可以拿這個參數(shù)進(jìn)行類增強等操作。

指定Main-Class

Agent需要打包成一個jar包,在ManiFest屬性中指定“Premain-Class”或者“Agent-Class”:

掛載到目標(biāo)JVM

將編寫的Agent打成jar包后,就可以掛載到目標(biāo)JVM上去了。如果選擇在目標(biāo)JVM啟動時加載Agent,則可以使用 “-javaagent:《jarpath》[=《option》]”,具體的使用方法可以使用“Java -Help”來查看。如果想要在運行時掛載Agent到目標(biāo)JVM,就需要做一些額外的開發(fā)了。

com.sun.tools.attach.VirtualMachine 這個類代表一個JVM抽象,可以通過這個類找到目標(biāo)JVM,并且將Agent掛載到目標(biāo)JVM上。下面是使用com.sun.tools.attach.VirtualMachine進(jìn)行動態(tài)掛載Agent的一般實現(xiàn):

首先通過指定的進(jìn)程ID找到目標(biāo)JVM,然后通過Attach掛載到目標(biāo)JVM上,執(zhí)行加載Agent操作。VirtualMachine的Attach方法就是用來將Agent掛載到目標(biāo)JVM上去的,而Detach則是將Agent從目標(biāo)JVM卸載。關(guān)于Agent是如何掛載到目標(biāo)JVM上的具體技術(shù)細(xì)節(jié),將在下文中進(jìn)行分析。

2.2 啟動時加載Agent

2.2.1 參數(shù)解析

創(chuàng)建JVM時,JVM會進(jìn)行參數(shù)解析,即解析那些用來配置JVM啟動的參數(shù),比如堆大小、GC等;本文主要關(guān)注解析的參數(shù)為-agentlib、 -agentpath、 -javaagent,這幾個參數(shù)用來指定Agent,JVM會根據(jù)這幾個參數(shù)加載Agent。下面來分析一下JVM是如何解析這幾個參數(shù)的。

上面的代碼片段截取自hotspot/src/share/vm/runtime/arguments.cpp中的Arguments::parse_each_vm_init_arg(const JavaVMInitArgs* args, bool* patch_mod_javabase, Flag::Flags origin) 函數(shù),該函數(shù)用來解析一個具體的JVM參數(shù)。這段代碼的主要功能是解析出需要加載的Agent路徑,然后調(diào)用add_init_agent函數(shù)進(jìn)行解析結(jié)果的存儲。下面先看一下add_init_agent函數(shù)的具體實現(xiàn):

AgentLibraryList是一個簡單的鏈表結(jié)構(gòu),add_init_agent函數(shù)將解析好的、需要加載的Agent添加到這個鏈表中,等待后續(xù)的處理。

這里需要注意,解析-javaagent參數(shù)有一些特別之處,這個參數(shù)用來指定一個我們通過Java Instrumentation API來編寫的Agent,Java Instrumentation API底層依賴的是JVMTI,對-JavaAgent的處理也說明了這一點,在調(diào)用add_init_agent函數(shù)時第一個參數(shù)是“instrument”,關(guān)于加載Agent這個問題在下一小節(jié)進(jìn)行展開。到此,我們知道在啟動JVM時指定的Agent已經(jīng)被JVM解析完存放在了一個鏈表結(jié)構(gòu)中。下面來分析一下JVM是如何加載這些Agent的。

2.2.2 執(zhí)行加載操作

在創(chuàng)建JVM進(jìn)程的函數(shù)中,解析完JVM參數(shù)之后,下面的這段代碼和加載Agent相關(guān):

當(dāng)JVM判斷出上一小節(jié)中解析出來的Agent不為空的時候,就要去調(diào)用函數(shù)create_vm_init_agents來加載Agent,下面來分析一下create_vm_init_agents函數(shù)是如何加載Agent的。

create_vm_init_agents這個函數(shù)通過遍歷Agent鏈表來逐個加載Agent。通過這段代碼可以看出,首先通過lookup_agent_on_load來加載Agent并且找到Agent_OnLoad函數(shù),這個函數(shù)是Agent的入口函數(shù)。如果沒找到這個函數(shù),則認(rèn)為是加載了一個不合法的Agent,則什么也不做,否則調(diào)用這個函數(shù),這樣Agent的代碼就開始執(zhí)行起來了。對于使用Java Instrumentation API來編寫Agent的方式來說,在解析階段觀察到在add_init_agent函數(shù)里面?zhèn)鬟f進(jìn)去的是一個叫做“instrument”的字符串,其實這是一個動態(tài)鏈接庫。在Linux里面,這個庫叫做libinstrument.so,在BSD系統(tǒng)中叫做libinstrument.dylib,該動態(tài)鏈接庫在{JAVA_HOME}/jre/lib/目錄下。

2.2.3 Instrument動態(tài)鏈接庫

libinstrument用來支持使用Java Instrumentation API來編寫Agent,在libinstrument中有一個非常重要的類稱為:JPLISAgent(Java Programming Language Instrumentation Services Agent),它的作用是初始化所有通過Java Instrumentation API編寫的Agent,并且也承擔(dān)著通過JVMTI實現(xiàn)Java Instrumentation中暴露API的責(zé)任。

我們已經(jīng)知道,在JVM啟動的時候,JVM會通過-javaagent參數(shù)加載Agent。最開始加載的是libinstrument動態(tài)鏈接庫,然后在動態(tài)鏈接庫里面找到JVMTI的入口方法:Agent_OnLoad。下面就來分析一下在libinstrument動態(tài)鏈接庫中,Agent_OnLoad函數(shù)是怎么實現(xiàn)的。

上述代碼片段是經(jīng)過精簡的libinstrument中Agent_OnLoad實現(xiàn)的,大概的流程就是:先創(chuàng)建一個JPLISAgent,然后將ManiFest中設(shè)定的一些參數(shù)解析出來, 比如(Premain-Class)等。創(chuàng)建了JPLISAgent之后,調(diào)用initializeJPLISAgent對這個Agent進(jìn)行初始化操作。跟進(jìn)initializeJPLISAgent看一下是如何初始化的:

這里,我們關(guān)注callbacks.VMInit = &eventHandlerVMInit;這行代碼,這里設(shè)置了一個VMInit事件的回調(diào)函數(shù),表示在JVM初始化的時候會回調(diào)eventHandlerVMInit函數(shù)。下面來看一下這個函數(shù)的實現(xiàn)細(xì)節(jié),猜測就是在這里調(diào)用了Premain方法:

看到這里,Instrument已經(jīng)實例化,invokeJavaAgentMainMethod這個方法將我們的Premain方法執(zhí)行起來了。接著,我們就可以根據(jù)Instrument實例來做我們想要做的事情了。

2.3 運行時加載Agent

比起JVM啟動時加載Agent,運行時加載Agent就比較有誘惑力了,因為運行時加載Agent的能力給我們提供了很強的動態(tài)性,我們可以在需要的時候加載Agent來進(jìn)行一些工作。因為是動態(tài)的,我們可以按照需求來加載所需要的Agent,下面來分析一下動態(tài)加載Agent的相關(guān)技術(shù)細(xì)節(jié)。

2.3.1 AttachListener

Attach機制通過Attach Listener線程來進(jìn)行相關(guān)事務(wù)的處理,下面來看一下Attach Listener線程是如何初始化的。

我們知道,一個線程啟動之后都需要指定一個入口來執(zhí)行代碼,Attach Listener線程的入口是attach_listener_thread_entry,下面看一下這個函數(shù)的具體實現(xiàn):

整個函數(shù)執(zhí)行邏輯,大概是這樣的:

拉取一個需要執(zhí)行的任務(wù):AttachListener::dequeue。

查詢匹配的命令處理函數(shù)。

執(zhí)行匹配到的命令執(zhí)行函數(shù)。

其中第二步里面存在一個命令函數(shù)表,整個表如下:

對于加載Agent來說,命令就是“l(fā)oad”?,F(xiàn)在,我們知道了Attach Listener大概的工作模式,但是還是不太清楚任務(wù)從哪來,這個秘密就藏在AttachListener::dequeue這行代碼里面,接下來我們來分析一下dequeue這個函數(shù):

這是Linux上的實現(xiàn),不同的操作系統(tǒng)實現(xiàn)方式不太一樣。上面的代碼表面,Attach Listener在某個端口監(jiān)聽著,通過accept來接收一個連接,然后從這個連接里面將請求讀取出來,然后將請求包裝成一個AttachOperation類型的對象,之后就會從表里查詢對應(yīng)的處理函數(shù),然后進(jìn)行處理。

Attach Listener使用一種被稱為“懶加載”的策略進(jìn)行初始化,也就是說,JVM啟動的時候Attach Listener并不一定會啟動起來。下面我們來分析一下這種“懶加載”策略的具體實現(xiàn)方案。

上面的代碼截取自create_vm函數(shù),DisableAttachMechanism、StartAttachListener和ReduceSignalUsage這三個變量默認(rèn)都是false,所以AttachListener::init();這行代碼不會在create_vm的時候執(zhí)行,而vm_start會執(zhí)行。下面來看一下這個函數(shù)的實現(xiàn)細(xì)節(jié):

這是在Linux上的實現(xiàn),是將/tmp/目錄下的.java_pid{pid}文件刪除,后面在創(chuàng)建Attach Listener線程的時候會創(chuàng)建出來這個文件。上面說到,AttachListener::init()這行代碼不會在create_vm的時候執(zhí)行,這行代碼的實現(xiàn)已經(jīng)在上文中分析了,就是創(chuàng)建Attach Listener線程,并監(jiān)聽其他JVM的命令請求。現(xiàn)在來分析一下這行代碼是什么時候被調(diào)用的,也就是“懶加載”到底是怎么加載起來的。

這是create_vm中的一段代碼,看起來跟信號相關(guān),其實Attach機制就是使用信號來實現(xiàn)“懶加載“的。下面我們來仔細(xì)地分析一下這個過程。

JVM創(chuàng)建了一個新的進(jìn)程來實現(xiàn)信號處理,這個線程叫“Signal Dispatcher”,一個線程創(chuàng)建之后需要有一個入口,“Signal Dispatcher”的入口是signal_thread_entry:

這段代碼截取自signal_thread_entry函數(shù),截取中的內(nèi)容是和Attach機制信號處理相關(guān)的代碼。這段代碼的意思是,當(dāng)接收到“SIGBREAK”信號,就執(zhí)行接下來的代碼,這個信號是需要Attach到JVM上的信號發(fā)出來,這個后面會再分析。我們先來看一句關(guān)鍵的代碼:AttachListener::is_init_trigger():

首先檢查了一下是否在JVM啟動時啟動了Attach Listener,或者是否已經(jīng)啟動過。如果沒有,才繼續(xù)執(zhí)行,在/tmp目錄下創(chuàng)建一個叫做.attach_pid%d的文件,然后執(zhí)行AttachListener的init函數(shù),這個函數(shù)就是用來創(chuàng)建Attach Listener線程的函數(shù),上面已經(jīng)提到多次并進(jìn)行了分析。到此,我們知道Attach機制的奧秘所在,也就是Attach Listener線程的創(chuàng)建依靠Signal Dispatcher線程,Signal Dispatcher是用來處理信號的線程,當(dāng)Signal Dispatcher線程接收到“SIGBREAK”信號之后,就會執(zhí)行初始化Attach Listener的工作。

2.3.2 運行時加載Agent的實現(xiàn)

我們繼續(xù)分析,到底是如何將一個Agent掛載到運行著的目標(biāo)JVM上,在上文中提到了一段代碼,用來進(jìn)行運行時掛載Agent,可以參考上文中展示的關(guān)于“attachAgentToTargetJvm”方法的代碼。這個方法里面的關(guān)鍵是調(diào)用VirtualMachine的attach方法進(jìn)行Agent掛載的功能。下面我們就來分析一下VirtualMachine的attach方法具體是怎么實現(xiàn)的。

這個方法通過attachVirtualMachine方法進(jìn)行attach操作,在MacOS系統(tǒng)中,AttachProvider的實現(xiàn)類是BsdAttachProvider。我們來看一下BsdAttachProvider的attachVirtualMachine方法是如何實現(xiàn)的:

findSocketFile方法用來查詢目標(biāo)JVM上是否已經(jīng)啟動了Attach Listener,它通過檢查“tmp/”目錄下是否存在java_pid{pid}來進(jìn)行實現(xiàn)。如果已經(jīng)存在了,則說明Attach機制已經(jīng)準(zhǔn)備就緒,可以接受客戶端的命令了,這個時候客戶端就可以通過connect連接到目標(biāo)JVM進(jìn)行命令的發(fā)送,比如可以發(fā)送“l(fā)oad”命令來加載Agent。如果java_pid{pid}文件還不存在,則需要通過sendQuitTo方法向目標(biāo)JVM發(fā)送一個“SIGBREAK”信號,讓它初始化Attach Listener線程并準(zhǔn)備接受客戶端連接。可以看到,發(fā)送了信號之后客戶端會循環(huán)等待java_pid{pid}這個文件,之后再通過connect連接到目標(biāo)JVM上。

2.3.3 load命令的實現(xiàn)

下面來分析一下,“l(fā)oad”命令在JVM層面的實現(xiàn):

這個函數(shù)先確保加載了java.instrument模塊,之后真正執(zhí)行Agent加載的函數(shù)是 load_agent_library ,這個函數(shù)的套路就是加載Agent動態(tài)鏈接庫,如果是通過Java instrument API實現(xiàn)的Agent,則加載的是libinstrument動態(tài)鏈接庫,然后通過libinstrument里面的代碼實現(xiàn)運行agentmain方法的邏輯,這一部分內(nèi)容和libinstrument實現(xiàn)premain方法運行的邏輯其實差不多,這里不再做分析。至此,我們對Java Agent技術(shù)已經(jīng)有了一個全面而細(xì)致的了解。

3. 動態(tài)替換類字節(jié)碼技術(shù)

3.1 動態(tài)字節(jié)碼修改的限制

上文中已經(jīng)詳細(xì)分析了Agent技術(shù)的實現(xiàn),我們使用Java Instrumentation API來完成動態(tài)類修改的功能,在Instrumentation接口中,通過addTransformer方法來增加一個類轉(zhuǎn)換器,類轉(zhuǎn)換器由類ClassFileTransformer接口實現(xiàn)。ClassFileTransformer接口中唯一的方法transform用于實現(xiàn)類轉(zhuǎn)換,當(dāng)類被加載的時候,就會調(diào)用transform方法,進(jìn)行類轉(zhuǎn)換。在運行時,我們可以通過Instrumentation的redefineClasses方法進(jìn)行類重定義,在方法上有一段注釋需要特別注意:

這里面提到,我們不可以增加、刪除或者重命名字段和方法,改變方法的簽名或者類的繼承關(guān)系。認(rèn)識到這一點很重要,當(dāng)我們通過ASM獲取到增強的字節(jié)碼之后,如果增強后的字節(jié)碼沒有遵守這些規(guī)則,那么調(diào)用redefineClasses方法來進(jìn)行類的重定義就會失敗。那redefineClasses方法具體是怎么實現(xiàn)類的重定義的呢?它對運行時的JVM會造成什么樣的影響呢?下面來分析redefineClasses的實現(xiàn)細(xì)節(jié)。

3.2 重定義類字節(jié)碼的實現(xiàn)細(xì)節(jié)

上文中我們提到,libinstrument動態(tài)鏈接庫中,JPLISAgent不僅實現(xiàn)了Agent入口代碼執(zhí)行的路由,而且還是Java代碼與JVMTI之間的一道橋梁。我們在Java代碼中調(diào)用Java Instrumentation API的redefineClasses,其實會調(diào)用libinstrument中的相關(guān)代碼,我們來分析一下這條路徑。

這是InstrumentationImpl中的redefineClasses實現(xiàn),該方法的具體實現(xiàn)依賴一個Native方法redefineClasses(),我們可以在libinstrument中找到這個Native方法的實現(xiàn):

redefineClasses這個函數(shù)的實現(xiàn)比較復(fù)雜,代碼很長。下面是一段關(guān)鍵的代碼片段:

可以看到,其實是調(diào)用了JVMTI的RetransformClasses函數(shù)來完成類的重定義細(xì)節(jié)。

重定義類的請求會被JVM包裝成一個VM_RedefineClasses類型的VM_Operation,VM_Operation是JVM內(nèi)部的一些操作的基類,包括GC操作等。VM_Operation由VMThread來執(zhí)行,新的VM_Operation操作會被添加到VMThread的運行隊列中去,VMThread會不斷從隊列里面拉取VM_Operation并調(diào)用其doit等函數(shù)執(zhí)行具體的操作。VM_RedefineClasses函數(shù)的流程較為復(fù)雜,下面是VM_RedefineClasses的大致流程:

加載新的字節(jié)碼,合并常量池,并且對新的字節(jié)碼進(jìn)行校驗工作

清除方法上的斷點

JIT逆優(yōu)化

進(jìn)行字節(jié)碼替換工作,需要進(jìn)行更新類itable/vtable等操作

進(jìn)行類重定義通知

VM_RedefineClasses實現(xiàn)比較復(fù)雜的,詳細(xì)實現(xiàn)可以參考 RedefineClasses的實現(xiàn)。

4. Java-debug-tool設(shè)計與實現(xiàn)

Java-debug-tool是一個使用Java Instrument API來實現(xiàn)的動態(tài)調(diào)試工具,它通過在目標(biāo)JVM上啟動一個TcpServer來和調(diào)試客戶端通信。調(diào)試客戶端通過命令行來發(fā)送調(diào)試命令給TcpServer,TcpServer中有專門用來處理命令的handler,handler處理完命令之后會將結(jié)果發(fā)送回客戶端,客戶端通過處理將調(diào)試結(jié)果展示出來。下面將詳細(xì)介紹Java-debug-tool的整體設(shè)計和實現(xiàn)。

4.1 Java-debug-tool整體架構(gòu)

Java-debug-tool包括一個Java Agent和一個用于處理調(diào)試命令的核心API,核心API通過一個自定義的類加載器加載進(jìn)來,以保證目標(biāo)JVM的類不會被污染。整體上Java-debug-tool的設(shè)計是一個Client-Server的架構(gòu),命令客戶端需要完整的完成一個命令之后才能繼續(xù)執(zhí)行下一個調(diào)試命令。Java-debug-tool支持多人同時進(jìn)行調(diào)試,下面是整體架構(gòu)圖:

圖4-1-1

下面對每一層做簡單介紹:

交互層:負(fù)責(zé)將程序員的輸入轉(zhuǎn)換成調(diào)試交互協(xié)議,并且將調(diào)試信息呈現(xiàn)出來。

連接管理層:負(fù)責(zé)管理客戶端連接,從連接中讀調(diào)試協(xié)議數(shù)據(jù)并解碼,對調(diào)試結(jié)果編碼并將其寫到連接中去;同時將那些超時未活動的連接關(guān)閉。

業(yè)務(wù)邏輯層:實現(xiàn)調(diào)試命令處理,包括命令分發(fā)、數(shù)據(jù)收集、數(shù)據(jù)處理等過程。

基礎(chǔ)實現(xiàn)層:Java-debug-tool實現(xiàn)的底層依賴,通過Java Instrumentation提供的API進(jìn)行類查找、類重定義等能力,Java Instrumentation底層依賴JVMTI來完成具體的功能。

在Agent被掛載到目標(biāo)JVM上之后,Java-debug-tool會安排一個Spy在目標(biāo)JVM內(nèi)活動,這個Spy負(fù)責(zé)將目標(biāo)JVM內(nèi)部的相關(guān)調(diào)試數(shù)據(jù)轉(zhuǎn)移到命令處理模塊,命令處理模塊會處理這些數(shù)據(jù),然后給客戶端返回調(diào)試結(jié)果。命令處理模塊會增強目標(biāo)類的字節(jié)碼來達(dá)到數(shù)據(jù)獲取的目的,多個客戶端可以共享一份增強過的字節(jié)碼,無需重復(fù)增強。下面從Java-debug-tool的字節(jié)碼增強方案、命令設(shè)計與實現(xiàn)等角度詳細(xì)說明。

4.2 Java-debug-tool的字節(jié)碼增強方案

Java-debug-tool使用字節(jié)碼增強來獲取到方法運行時的信息,比如方法入?yún)?、出參等,可以在不同的字?jié)碼位置進(jìn)行增強,這種行為可以稱為“插樁”,每個“樁”用于獲取數(shù)據(jù)并將他轉(zhuǎn)儲出去。Java-debug-tool具備強大的插樁能力,不同的樁負(fù)責(zé)獲取不同類別的數(shù)據(jù),下面是Java-debug-tool目前所支持的“樁”:

方法進(jìn)入點:用于獲取方法入?yún)⑿畔ⅰ?/p>

Fields獲取點1:在方法執(zhí)行前獲取到對象的字段信息。

變量存儲點:獲取局部變量信息。

Fields獲取點2:在方法退出前獲取到對象的字段信息。

方法退出點:用于獲取方法返回值。

拋出異常點:用于獲取方法拋出的異常信息。

通過上面這些代碼樁,Java-debug-tool可以收集到豐富的方法執(zhí)行信息,經(jīng)過處理可以返回更加可視化的調(diào)試結(jié)果。

4.2.1 字節(jié)碼增強

Java-debug-tool在實現(xiàn)上使用了ASM工具來進(jìn)行字節(jié)碼增強,并且每個插樁點都可以進(jìn)行配置,如果不想要什么信息,則沒必要進(jìn)行對應(yīng)的插樁操作。這種可配置的設(shè)計是非常有必要的,因為有時候我們僅僅是想要知道方法的入?yún)⒑统鰠?,但Java-debug-tool卻給我們返回了所有的調(diào)試信息,這樣我們就得在眾多的輸出中找到我們所關(guān)注的內(nèi)容。如果可以進(jìn)行配置,則除了入?yún)Ⅻc和出參點外其他的樁都不插,那么就可以快速看到我們想要的調(diào)試數(shù)據(jù),這種設(shè)計的本質(zhì)是為了讓調(diào)試者更加專注。下面是Java-debug-tool的字節(jié)碼增強工作方式:

圖4-2-1

如圖4-2-1所示,當(dāng)調(diào)試者發(fā)出調(diào)試命令之后,Java-debug-tool會識別命令并判斷是否需要進(jìn)行字節(jié)碼增強,如果命令需要增強字節(jié)碼,則判斷當(dāng)前類+當(dāng)前方法是否已經(jīng)被增強過。上文已經(jīng)提到,字節(jié)碼替換是有一定損耗的,這種具有損耗的操作發(fā)生的次數(shù)越少越好,所以字節(jié)碼替換操作會被記錄起來,后續(xù)命令直接使用即可,不需要重復(fù)進(jìn)行字節(jié)碼增強,字節(jié)碼增強還涉及多個調(diào)試客戶端的協(xié)同工作問題,當(dāng)一個客戶端增強了一個類的字節(jié)碼之后,這個客戶端就鎖定了該字節(jié)碼,其他客戶端變成只讀,無法對該類進(jìn)行字節(jié)碼增強,只有當(dāng)持有鎖的客戶端主動釋放鎖或者斷開連接之后,其他客戶端才能繼續(xù)增強該類的字節(jié)碼。

字節(jié)碼增強模塊收到字節(jié)碼增強請求之后,會判斷每個增強點是否需要插樁,這個判斷的根據(jù)就是上文提到的插樁配置,之后字節(jié)碼增強模塊會生成新的字節(jié)碼,Java-debug-tool將執(zhí)行字節(jié)碼替換操作,之后就可以進(jìn)行調(diào)試數(shù)據(jù)收集了。

經(jīng)過字節(jié)碼增強之后,原來的方法中會插入收集運行時數(shù)據(jù)的代碼,這些代碼在方法被調(diào)用的時候執(zhí)行,獲取到諸如方法入?yún)?、局部變量等信息,這些信息將傳遞給數(shù)據(jù)收集裝置進(jìn)行處理。數(shù)據(jù)收集的工作通過Advice完成,每個客戶端同一時間只能注冊一個Advice到Java-debug-tool調(diào)試模塊上,多個客戶端可以同時注冊自己的Advice到調(diào)試模塊上。Advice負(fù)責(zé)收集數(shù)據(jù)并進(jìn)行判斷,如果當(dāng)前數(shù)據(jù)符合調(diào)試命令的要求,Java-debug-tool就會卸載這個Advice,Advice的數(shù)據(jù)就會被轉(zhuǎn)移到Java-debug-tool的命令結(jié)果處理模塊進(jìn)行處理,并將結(jié)果發(fā)送到客戶端。

4.2.2 Advice的工作方式

Advice是調(diào)試數(shù)據(jù)收集器,不同的調(diào)試策略會對應(yīng)不同的Advice。Advice是工作在目標(biāo)JVM的線程內(nèi)部的,它需要輕量級和高效,意味著Advice不能做太過于復(fù)雜的事情,它的核心接口“match”用來判斷本次收集到的調(diào)試數(shù)據(jù)是否滿足調(diào)試需求。如果滿足,那么Java-debug-tool就會將其卸載,否則會繼續(xù)讓他收集調(diào)試數(shù)據(jù),這種“加載Advice” -》 “卸載Advice”的工作模式具備很好的靈活性。

關(guān)于Advice,需要說明的另外一點就是線程安全,因為它加載之后會運行在目標(biāo)JVM的線程中,目標(biāo)JVM的方法極有可能是多線程訪問的,這也就是說,Advice需要有能力處理多個線程同時訪問方法的能力,如果Advice處理不當(dāng),則可能會收集到雜亂無章的調(diào)試數(shù)據(jù)。下面的圖片展示了Advice和Java-debug-tool調(diào)試分析模塊、目標(biāo)方法執(zhí)行以及調(diào)試客戶端等模塊的關(guān)系。

圖4-2-2

Advice的首次掛載由Java-debug-tool的命令處理器完成,當(dāng)一次調(diào)試數(shù)據(jù)收集完成之后,調(diào)試數(shù)據(jù)處理模塊會自動卸載Advice,然后進(jìn)行判斷,如果調(diào)試數(shù)據(jù)符合Advice的策略,則直接將數(shù)據(jù)交由數(shù)據(jù)處理模塊進(jìn)行處理,否則會清空調(diào)試數(shù)據(jù),并再次將Advice掛載到目標(biāo)方法上去,等待下一次調(diào)試數(shù)據(jù)。非首次掛載由調(diào)試數(shù)據(jù)處理模塊進(jìn)行,它借助Advice按需取數(shù)據(jù),如果不符合需求,則繼續(xù)掛載Advice來獲取數(shù)據(jù),否則對調(diào)試數(shù)據(jù)進(jìn)行處理并返回給客戶端。

4.3 Java-debug-tool的命令設(shè)計與實現(xiàn)

4.3.1 命令執(zhí)行

上文已經(jīng)完整的描述了Java-debug-tool的設(shè)計以及核心技術(shù)方案,本小節(jié)將詳細(xì)介紹Java-debug-tool的命令設(shè)計與實現(xiàn)。首先需要將一個調(diào)試命令的執(zhí)行流程描述清楚,下面是一張用來表示命令請求處理流程的圖片:

圖4-3-1

圖4-3-1簡單的描述了Java-debug-tool的命令處理方式,客戶端連接到服務(wù)端之后,會進(jìn)行一些協(xié)議解析、協(xié)議認(rèn)證、協(xié)議填充等工作,之后將進(jìn)行命令分發(fā)。服務(wù)端如果發(fā)現(xiàn)客戶端的命令不合法,則會立即返回錯誤信息,否則再進(jìn)行命令處理。命令處理屬于典型的三段式處理,前置命令處理、命令處理以及后置命令處理,同時會對命令處理過程中的異常信息進(jìn)行捕獲處理,三段式處理的好處是命令處理被拆成了多個階段,多個階段負(fù)責(zé)不同的職責(zé)。前置命令處理用來做一些命令權(quán)限控制的工作,并填充一些類似命令處理開始時間戳等信息,命令處理就是通過字節(jié)碼增強,掛載Advice進(jìn)行數(shù)據(jù)收集,再經(jīng)過數(shù)據(jù)處理來產(chǎn)生命令結(jié)果的過程,后置處理則用來處理一些連接關(guān)閉、字節(jié)碼解鎖等事項。

Java-debug-tool允許客戶端設(shè)置一個命令執(zhí)行超時時間,超過這個時間則認(rèn)為命令沒有結(jié)果,如果客戶端沒有設(shè)置自己的超時時間,就使用默認(rèn)的超時時間進(jìn)行超時控制。Java-debug-tool通過設(shè)計了兩階段的超時檢測機制來實現(xiàn)命令執(zhí)行超時功能:首先,第一階段超時觸發(fā),則Java-debug-tool會友好的警告命令處理模塊處理時間已經(jīng)超時,需要立即停止命令執(zhí)行,這允許命令自己做一些現(xiàn)場清理工作,當(dāng)然需要命令執(zhí)行線程自己感知到這種超時警告;當(dāng)?shù)诙A段超時觸發(fā),則Java-debug-tool認(rèn)為命令必須結(jié)束執(zhí)行,會強行打斷命令執(zhí)行線程。超時機制的目的是為了不讓命令執(zhí)行太長時間,命令如果長時間沒有收集到調(diào)試數(shù)據(jù),則應(yīng)該停止執(zhí)行,并思考是否調(diào)試了一個錯誤的方法。當(dāng)然,超時機制還可以定期清理那些因為未知原因斷開連接的客戶端持有的調(diào)試資源,比如字節(jié)碼鎖。

4.3.4 獲取方法執(zhí)行視圖

Java-debug-tool通過下面的信息來向調(diào)試者呈現(xiàn)出一次方法執(zhí)行的視圖:

正在調(diào)試的方法信息。

方法調(diào)用堆棧。

調(diào)試耗時,包括對目標(biāo)JVM造成的STW時間。

方法入?yún)?,包括入?yún)⒌念愋图皡?shù)值。

方法的執(zhí)行路徑。

代碼執(zhí)行耗時。

局部變量信息。

方法返回結(jié)果。

方法拋出的異常。

對象字段值快照。

圖4-3-2展示了Java-debug-tool獲取到正在運行的方法的執(zhí)行視圖的信息。

圖4-3-2

4.4 Java-debug-tool與同類產(chǎn)品對比分析

Java-debug-tool的同類產(chǎn)品主要是greys,其他類似的工具大部分都是基于greys進(jìn)行的二次開發(fā),所以直接選擇greys來和Java-debug-tool進(jìn)行對比。

5. 總結(jié)

本文詳細(xì)剖析了Java動態(tài)調(diào)試關(guān)鍵技術(shù)的實現(xiàn)細(xì)節(jié),并介紹了我們基于Java動態(tài)調(diào)試技術(shù)結(jié)合實際故障排查場景進(jìn)行的一點探索實踐;動態(tài)調(diào)試技術(shù)為研發(fā)人員進(jìn)行線上問題排查提供了一種新的思路,我們基于動態(tài)調(diào)試技術(shù)解決了傳統(tǒng)斷點調(diào)試存在的問題,使得可以將斷點調(diào)試這種技術(shù)應(yīng)用在線上,以線下調(diào)試的思維來進(jìn)行線上調(diào)試,提高問題排查效率。
責(zé)任編輯人:CC

聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問題,請聯(lián)系本站處理。 舉報投訴
  • JAVA
    +關(guān)注

    關(guān)注

    20

    文章

    2989

    瀏覽量

    109572
  • 動態(tài)調(diào)試
    +關(guān)注

    關(guān)注

    0

    文章

    2

    瀏覽量

    5811
收藏 人收藏
加入交流群
微信小助手二維碼

掃碼添加小助手

加入工程師交流群

    評論

    相關(guān)推薦
    熱點推薦

    技術(shù)干貨 | AD/DA動態(tài)分析中的信號窗口處理技術(shù)

    詳解了TX7006上的線性計算,AD/DA動態(tài)分析中的傅里葉變換和動態(tài)參數(shù)計算。本期文章將為大家繼續(xù)介紹AD/DA動態(tài)分析中的信號窗口
    的頭像 發(fā)表于 07-03 13:39 ?503次閱讀
    <b class='flag-5'>技術(shù)</b>干貨 | AD/DA<b class='flag-5'>動態(tài)</b>分析中的信號窗口處理<b class='flag-5'>技術(shù)</b>

    詳解外延生長技術(shù)

    隨著半導(dǎo)體器件特征尺寸不斷微縮,對高質(zhì)量薄膜材料的需求愈發(fā)迫切。外延技術(shù)作為種在半導(dǎo)體工藝制造中常用的單晶薄膜生長方法,能夠在單晶襯底上按襯底晶向生長新的單晶薄膜,為提升器件性能發(fā)揮了關(guān)鍵作用。本文將對外延技術(shù)的定義、分類、原
    的頭像 發(fā)表于 06-16 11:44 ?948次閱讀
    <b class='flag-5'>一</b><b class='flag-5'>文</b><b class='flag-5'>詳解</b>外延生長<b class='flag-5'>技術(shù)</b>

    安徽京準(zhǔn):北斗衛(wèi)星同步時鐘的安裝與調(diào)試詳解

    安徽京準(zhǔn):北斗衛(wèi)星同步時鐘的安裝與調(diào)試詳解
    的頭像 發(fā)表于 06-05 10:08 ?415次閱讀
    安徽京準(zhǔn):北斗衛(wèi)星同步時鐘的安裝與<b class='flag-5'>調(diào)試</b><b class='flag-5'>詳解</b>

    【必看】開關(guān)電源中每個元器件的計算+51頁圖文詳解

    開關(guān)電源的各個元器件怎么計算?損耗怎么估算?散熱器的大小怎么計算? 51頁圖文詳解,帶你弄懂! 純分享貼,有需要可以直接下載附件獲取完整資料! (如果內(nèi)容有幫助可以關(guān)注、點贊、評論支持
    發(fā)表于 05-12 16:20

    Java開發(fā)者必備的效率工具——Perforce JRebel是什么?為什么很多Java開發(fā)者在用?

    Perforce JRebel是Java開發(fā)效率工具,旨在幫助java開發(fā)人員更快地編寫更好的應(yīng)用程序。JRebel可即時重新加載對代碼的修改,無需重啟或重新部署應(yīng)用程序,就能讓開發(fā)者即時看到代碼更改的效果,從而縮短開發(fā)、
    的頭像 發(fā)表于 04-27 13:44 ?257次閱讀
    <b class='flag-5'>Java</b>開發(fā)者必備的效率工具——Perforce JRebel是什么?為什么很多<b class='flag-5'>Java</b>開發(fā)者在用?

    詳解多芯片堆疊技術(shù)

    多芯片堆疊技術(shù)的出現(xiàn),順應(yīng)了器件朝著小型化、集成化方向發(fā)展的趨勢。該技術(shù)與先進(jìn)封裝領(lǐng)域中的系統(tǒng)級封裝(SIP)存在定差異。
    的頭像 發(fā)表于 04-12 14:22 ?952次閱讀
    <b class='flag-5'>一</b><b class='flag-5'>文</b><b class='flag-5'>詳解</b>多芯片堆疊<b class='flag-5'>技術(shù)</b>

    Java的SPI機制詳解

    作者:京東物流 楊葦葦 1.SPI簡介 SPI(Service Provicer Interface)是Java語言提供的種接口發(fā)現(xiàn)機制,用來實現(xiàn)接口和接口實現(xiàn)的解耦。簡單來說,就是系統(tǒng)只需要定義
    的頭像 發(fā)表于 03-05 11:35 ?779次閱讀
    <b class='flag-5'>Java</b>的SPI機制<b class='flag-5'>詳解</b>

    對比Python與Java編程語言

    Python與Java都是目前非常流行的編程語言,它們各有其獨特的優(yōu)勢和適用場景。以下是對這兩種編程語言的對比: 、語法和易用性 Python 語法簡潔,代碼更易讀,非常適合初學(xué)者。 動態(tài)類型系統(tǒng)
    的頭像 發(fā)表于 11-15 09:31 ?1074次閱讀

    詳解RAID技術(shù)

    RAID(Redundant Array of Independent Disks)即獨立磁盤冗余陣列,RAID技術(shù)將多個單獨的物理硬盤以不同的方式組合成個邏輯硬盤,從而提高硬盤的讀寫性能和數(shù)據(jù)安全性。
    的頭像 發(fā)表于 11-06 18:06 ?1276次閱讀
    <b class='flag-5'>一</b><b class='flag-5'>文</b><b class='flag-5'>詳解</b>RAID<b class='flag-5'>技術(shù)</b>

    智慧公交是什么?帶你詳解智慧公交的解決方案!

    智慧公交是什么?帶你詳解智慧公交的解決方案!
    的頭像 發(fā)表于 11-05 12:26 ?960次閱讀
    智慧公交是什么?<b class='flag-5'>一</b><b class='flag-5'>文</b>帶你<b class='flag-5'>詳解</b>智慧公交的解決方案!

    CPK為什么要大于1.33?詳解CPK計算

    原文標(biāo)題:CPK為什么要大于1.33?詳解CPK計算
    的頭像 發(fā)表于 11-01 11:08 ?1156次閱讀

    甲骨發(fā)布Java 23

    全球領(lǐng)先的軟件開發(fā)企業(yè)甲骨(Oracle)近日隆重宣布推出Java 23(Oracle JDK 23),這里程碑式的更新標(biāo)志著全球排名第的編程語言和開發(fā)平臺再次邁出堅實步伐。
    的頭像 發(fā)表于 09-19 16:36 ?658次閱讀

    弄懂實時動態(tài)載波相位差分技術(shù)和偽距差分技術(shù)的區(qū)別

    在全球?qū)Ш叫l(wèi)星系統(tǒng)(GNSS)中,實時動態(tài)載波相位差分和偽距差分是常見的兩種差分定位技術(shù)。這兩種技術(shù)在定位精度、可用性和適用性等方面存在著些明顯的區(qū)別。本文將介紹實時
    的頭像 發(fā)表于 09-13 11:14 ?1855次閱讀
    <b class='flag-5'>一</b><b class='flag-5'>文</b>弄懂實時<b class='flag-5'>動態(tài)</b>載波相位差分<b class='flag-5'>技術(shù)</b>和偽距差分<b class='flag-5'>技術(shù)</b>的區(qū)別

    詳解動態(tài)多點VPN技術(shù)

    引言 動態(tài)多點VPN(Dynamic Multipoint VPN)是mGRE、NHRP(Next Hop Resolution Protocol)、IPSec結(jié)合產(chǎn)生的技術(shù),簡寫為DMVPN
    發(fā)表于 07-26 06:07

    華納云:java web和java有什么區(qū)別java web和java有什么區(qū)別

    的平臺,Java可以用于開發(fā)桌面應(yīng)用程序、移動應(yīng)用程序、企業(yè)級應(yīng)用程序等。 – Java Web是Java語言在Web開發(fā)領(lǐng)域的應(yīng)用,它使用Java
    的頭像 發(fā)表于 07-16 13:35 ?1447次閱讀
    華納云:<b class='flag-5'>java</b> web和<b class='flag-5'>java</b>有什么區(qū)別<b class='flag-5'>java</b> web和<b class='flag-5'>java</b>有什么區(qū)別