JVM原始碼分析之Attach機制實現完全解讀

PerfMa發表於2020-05-26
本文來自: PerfMa技術社群

PerfMa(笨馬網路)官網

Attach是什麼

在講這個之前,我們先來點大家都知道的東西,當我們感覺執行緒一直卡在某個地方,想知道卡在哪裡,首先想到的是進行執行緒dump,而常用的命令是jstack ,我們就可以看到如下執行緒棧了

image.png

大家是否注意過上面圈起來的兩個執行緒,”Attach Listener”和“Signal Dispatcher”,這兩個執行緒是我們這次要講的Attach機制的關鍵,先偷偷告訴各位,其實Attach Listener這個執行緒在jvm起來的時候可能並沒有的,後面會細說。

那Attach機制是什麼?說簡單點就是jvm提供一種jvm程式間通訊的能力,能讓一個程式傳命令給另外一個程式,並讓它執行內部的一些操作,比如說我們為了讓另外一個jvm程式把執行緒dump出來,那麼我們跑了一個jstack的程式,然後傳了個pid的引數,告訴它要哪個程式進行執行緒dump,既然是兩個程式,那肯定涉及到程式間通訊,以及傳輸協議的定義,比如要執行什麼操作,傳了什麼引數等

Attach能做些什麼

總結起來說,比如記憶體dump,執行緒dump,類資訊統計(比如載入的類及大小以及例項個數等),動態載入agent(使用過btrace的應該不陌生),動態設定vm flag(但是並不是所有的flag都可以設定的,因為有些flag是在jvm啟動過程中使用的,是一次性的),列印vm flag,獲取系統屬性等,這些對應的原始碼(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 }
};

後面是命令對應的處理函式。

Attach在jvm裡如何實現的

Attach Listener執行緒的建立

前面也提到了,jvm在啟動過程中可能並沒有啟動Attach Listener這個執行緒,可以通過jvm引數來啟動,程式碼 (Threads::create_vm)如下:

  if (!DisableAttachMechanism) {
    if (StartAttachListener || AttachListener::init_at_startup()) {
      AttachListener::init();
    }
  }
bool AttachListener::init_at_startup() {
  if (ReduceSignalUsage) {
    return true;
  } else {
    return false;
  }
}

其中DisableAttachMechanism,StartAttachListener ,ReduceSignalUsage均預設是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()並不會被執行,而Attach Listener執行緒正是在此方法裡建立的

image.png

既然在啟動的時候不會建立這個執行緒,那麼我們在上面看到的那個執行緒是怎麼建立的呢,這個就要關注另外一個執行緒“Signal Dispatcher”了,顧名思義是處理訊號的,這個執行緒是在jvm啟動的時候就會建立的,具體程式碼就不說了。

下面以jstack的實現來說明觸發Attach這一機制進行的過程,jstack命令的實現其實是一個叫做JStack.java的類,檢視jstack程式碼後會走到下面的方法裡

image.png

請注意VirtualMachine.Attach(pid);這行程式碼,觸發Attach pid的關鍵,如果是在linux下會走到下面的建構函式

image.png

這裡要解釋下程式碼了,首先看到呼叫了createAttachFile方法在目標程式的cwd目錄下建立了一個檔案/proc//cwd/.Attach_pid,這個在後面的訊號處理過程中會取出來做判斷(為了安全),另外我們知道在linux下執行緒是用程式實現的,在jvm啟動過程中會建立很多執行緒,比如我們上面的訊號執行緒,也就是會看到很多的pid(應該是LWP),那麼如何找到這個訊號處理執行緒呢,從上面實現來看是找到我們傳進去的pid的父程式,然後給它的所有子程式都傳送一個SIGQUIT訊號,而jvm裡除了訊號執行緒,其他執行緒都設定了對此訊號的遮蔽,因此收不到該訊號,於是該訊號就傳給了“Signal Dispatcher”,在傳完之後作輪詢等待看目標程式是否建立了某個檔案,AttachTimeout預設超時時間是5000ms,可通過設定系統變數sun.tools.Attach.AttachTimeout來指定,下面是Signal Dispatcher執行緒的entry實現

image.png

當訊號是SIGBREAK(在jvm裡做了#define,其實就是SIGQUIT)的時候,就會觸發
AttachListener::is_init_trigger()的執行

image.png

一開始會判斷當前程式目錄下是否有個.Attach_pid檔案(前面提到了),如果沒有就會在/tmp下建立一個/tmp/.Attach_pid,當那個檔案的uid和自己的uid是一致的情況下(為了安全)再呼叫init方法

image.png

此時水落石出了,看到建立了一個執行緒,並且取名為Attach Listener。再看看其子類LinuxAttachListener的init方法

image.png

看到其建立了一個監聽套接字,並建立了一個檔案/tmp/.java_pid,這個檔案就是客戶端之前一直在輪詢等待的檔案,隨著這個檔案的生成,意味著Attach的過程圓滿結束了。

Attach listener接收請求

看看它的entry實現Attach_listener_thread_entry

image.png

從程式碼來看就是從佇列裡不斷取AttachOperation,然後找到請求命令對應的方法進行執行,比如我們一開始說的jstack命令,找到 { “threaddump”, thread_dump }的對映關係,然後執行thread_dump方法

再來看看其要呼叫的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;
}

最終呼叫的是LinuxAttachListener::dequeue(),

image.png

我們看到如果沒有請求的話,會一直accept在那裡,當來了請求,然後就會建立一個套接字,並讀取資料,構建出LinuxAttachOperation返回並執行。

整個過程就這樣了,從Attach執行緒建立到接收請求,處理請求。

一起來學習吧

PerfMa KO 系列課之 JVM 引數【Memory篇】

記一次微服務耗時毛刺排查

相關文章