JVM源碼分析之Attach機(jī)制實(shí)現(xiàn)完全解讀
Attach是什么
在講這個(gè)之前,我們先來(lái)點(diǎn)大家都知道的東西,當(dāng)我們感覺(jué)線程一直卡在某個(gè)地方,想知道卡在哪里,首先想到的是進(jìn)行線程dump,而常用的命令是jstack ,我們就可以看到如下線程棧了
大家是否注意過(guò)上面圈起來(lái)的兩個(gè)線程,”Attach Listener”和“Signal Dispatcher”,這兩個(gè)線程是我們這次要講的Attach機(jī)制的關(guān)鍵,先偷偷告訴各位,其實(shí)Attach Listener這個(gè)線程在jvm起來(lái)的時(shí)候可能并沒(méi)有的,后面會(huì)細(xì)說(shuō)。
那Attach機(jī)制是什么?說(shuō)簡(jiǎn)單點(diǎn)就是jvm提供一種jvm進(jìn)程間通信的能力,能讓一個(gè)進(jìn)程傳命令給另外一個(gè)進(jìn)程,并讓它執(zhí)行內(nèi)部的一些操作,比如說(shuō)我們?yōu)榱俗屃硗庖粋€(gè)jvm進(jìn)程把線程dump出來(lái),那么我們跑了一個(gè)jstack的進(jìn)程,然后傳了個(gè)pid的參數(shù),告訴它要哪個(gè)進(jìn)程進(jìn)行線程dump,既然是兩個(gè)進(jìn)程,那肯定涉及到進(jìn)程間通信,以及傳輸協(xié)議的定義,比如要執(zhí)行什么操作,傳了什么參數(shù)等
Attach能做些什么
總結(jié)起來(lái)說(shuō),比如內(nèi)存dump,線程dump,類信息統(tǒng)計(jì)(比如加載的類及大小以及實(shí)例個(gè)數(shù)等),動(dòng)態(tài)加載agent(使用過(guò)btrace的應(yīng)該不陌生),動(dòng)態(tài)設(shè)置vm flag(但是并不是所有的flag都可以設(shè)置的,因?yàn)橛行ゝlag是在jvm啟動(dòng)過(guò)程中使用的,是一次性的),打印vm flag,獲取系統(tǒng)屬性等,這些對(duì)應(yīng)的源碼(AttachListener.cpp)如下
- static AttachOperationFunctionInfo funcs[] = {
- { "agentProperties", get_agent_properties },
- { "datadump", data_dump },
- { "dumpheap", dump_heap },
- { "load", JvmtiExport::load_agent_library },
- { "properties", get_system_properties },
- { "threaddump", thread_dump },
- { "inspectheap", heap_inspection },
- { "setflag", set_flag },
- { "printflag", print_flag },
- { "jcmd", jcmd },
- { NULL, NULL }
- };
后面是命令對(duì)應(yīng)的處理函數(shù)。
Attach在jvm里如何實(shí)現(xiàn)的
Attach Listener線程的創(chuàng)建
前面也提到了,jvm在啟動(dòng)過(guò)程中可能并沒(méi)有啟動(dòng)Attach Listener這個(gè)線程,可以通過(guò)jvm參數(shù)來(lái)啟動(dòng),代碼 (Threads::create_vm)如下:
- if (!DisableAttachMechanism) {
- if (StartAttachListener || AttachListener::init_at_startup()) {
- AttachListener::init();
- }
- }
- ool AttachListener::init_at_startup() {
- if (ReduceSignalUsage) {
- return true;
- } else {
- return false;
- }
其中DisableAttachMechanism,StartAttachListener ,ReduceSignalUsage均默認(rèn)是false(globals.hpp)
- product(bool, DisableAttachMechanism, false,
- "Disable mechanism that allows tools to Attach to this VM”)
- product(bool, StartAttachListener, false,
- "Always start Attach Listener at VM startup")
- product(bool, ReduceSignalUsage, false,
- "Reduce the use of OS signals in Java and/or the VM”)
因此AttachListener::init()并不會(huì)被執(zhí)行,而Attach Listener線程正是在此方法里創(chuàng)建的
既然在啟動(dòng)的時(shí)候不會(huì)創(chuàng)建這個(gè)線程,那么我們?cè)谏厦婵吹降哪莻€(gè)線程是怎么創(chuàng)建的呢,這個(gè)就要關(guān)注另外一個(gè)線程“Signal Dispatcher”了,顧名思義是處理信號(hào)的,這個(gè)線程是在jvm啟動(dòng)的時(shí)候就會(huì)創(chuàng)建的,具體代碼就不說(shuō)了。
下面以jstack的實(shí)現(xiàn)來(lái)說(shuō)明觸發(fā)Attach這一機(jī)制進(jìn)行的過(guò)程,jstack命令的實(shí)現(xiàn)其實(shí)是一個(gè)叫做JStack.java的類,查看jstack代碼后會(huì)走到下面的方法里
請(qǐng)注意VirtualMachine.Attach(pid);這行代碼,觸發(fā)Attach pid的關(guān)鍵,如果是在linux下會(huì)走到下面的構(gòu)造函數(shù)
這里要解釋下代碼了,首先看到調(diào)用了createAttachFile方法在目標(biāo)進(jìn)程的cwd目錄下創(chuàng)建了一個(gè)文件/proc//cwd/.Attach_pid,這個(gè)在后面的信號(hào)處理過(guò)程中會(huì)取出來(lái)做判斷(為了安全),另外我們知道在linux下線程是用進(jìn)程實(shí)現(xiàn)的,在jvm啟動(dòng)過(guò)程中會(huì)創(chuàng)建很多線程,比如我們上面的信號(hào)線程,也就是會(huì)看到很多的pid(應(yīng)該是LWP),那么如何找到這個(gè)信號(hào)處理線程呢,從上面實(shí)現(xiàn)來(lái)看是找到我們傳進(jìn)去的pid的父進(jìn)程,然后給它的所有子進(jìn)程都發(fā)送一個(gè)SIGQUIT信號(hào),而jvm里除了信號(hào)線程,其他線程都設(shè)置了對(duì)此信號(hào)的屏蔽,因此收不到該信號(hào),于是該信號(hào)就傳給了“Signal Dispatcher”,在傳完之后作輪詢等待看目標(biāo)進(jìn)程是否創(chuàng)建了某個(gè)文件,AttachTimeout默認(rèn)超時(shí)時(shí)間是5000ms,可通過(guò)設(shè)置系統(tǒng)變量sun.tools.Attach.AttachTimeout來(lái)指定,下面是Signal Dispatcher線程的entry實(shí)現(xiàn)
當(dāng)信號(hào)是SIGBREAK(在jvm里做了#define,其實(shí)就是SIGQUIT)的時(shí)候,就會(huì)觸發(fā)
AttachListener::is_init_trigger()的執(zhí)行
一開(kāi)始會(huì)判斷當(dāng)前進(jìn)程目錄下是否有個(gè).Attach_pid文件(前面提到了),如果沒(méi)有就會(huì)在/tmp下創(chuàng)建一個(gè)/tmp/.Attach_pid,當(dāng)那個(gè)文件的uid和自己的uid是一致的情況下(為了安全)再調(diào)用init方法
此時(shí)水落石出了,看到創(chuàng)建了一個(gè)線程,并且取名為Attach Listener。再看看其子類LinuxAttachListener的init方法
看到其創(chuàng)建了一個(gè)監(jiān)聽(tīng)套接字,并創(chuàng)建了一個(gè)文件/tmp/.java_pid,這個(gè)文件就是客戶端之前一直在輪詢等待的文件,隨著這個(gè)文件的生成,意味著Attach的過(guò)程圓滿結(jié)束了。
Attach listener接收請(qǐng)求
看看它的entry實(shí)現(xiàn)Attach_listener_thread_entry
從代碼來(lái)看就是從隊(duì)列里不斷取AttachOperation,然后找到請(qǐng)求命令對(duì)應(yīng)的方法進(jìn)行執(zhí)行,比如我們一開(kāi)始說(shuō)的jstack命令,找到 { “threaddump”, thread_dump }的映射關(guān)系,然后執(zhí)行thread_dump方法
再來(lái)看看其要調(diào)用的AttachListener::dequeue(),
- AttachOperation* AttachListener::dequeue() {
- JavaThread* thread = JavaThread::current();
- ThreadBlockInVM tbivm(thread);
- thread->set_suspend_equivalent();
- // cleared by handle_special_suspend_equivalent_condition() or
- // java_suspend_self() via check_and_wait_while_suspended()
- AttachOperation* op = LinuxAttachListener::dequeue();
- // were we externally suspended while we were waiting?
- thread->check_and_wait_while_suspended();
- return op;
- }
最終調(diào)用的是LinuxAttachListener::dequeue(),
我們看到如果沒(méi)有請(qǐng)求的話,會(huì)一直accept在那里,當(dāng)來(lái)了請(qǐng)求,然后就會(huì)創(chuàng)建一個(gè)套接字,并讀取數(shù)據(jù),構(gòu)建出LinuxAttachOperation返回并執(zhí)行。
整個(gè)過(guò)程就這樣了,從Attach線程創(chuàng)建到接收請(qǐng)求,處理請(qǐng)求。