1. 程式人生 > 程式設計 >jvm中的safepoint

jvm中的safepoint

前言

多執行緒程式設計是一件很難的事,或者說編寫在多執行緒條件下執行良好的程式碼很難。java提供了synchronized和volatile關鍵字,還有Lock類和Atomic相關的類來幫助我們正確的實現併發邏輯,但我在實際工作中仍傾向於儘量避免併發,還有一個偷懶的做法就是需要併發訪問的變數總是加上volatile修飾。

最近遇到了兩個併發相關的例子,一個是某個同事編寫的利用AtomicInteger類實現的lock-free邏輯出現了bug,由於邊界條件沒處理好,導致出現死迴圈的情況;另一個是某個服務由於平時負載比較高,出現了偏向鎖撤銷耗時過長的問題。

偏向鎖撤銷

偏向鎖實際上是jvm對鎖的一種優化,它假定對於一個鎖,實際上只有一個執行緒在嘗試訪問。偏向鎖的實現很簡單,就是在一個執行緒訪問鎖時,將這個鎖的持有者直接標記為這個執行緒,當這個執行緒再嘗試獲取鎖時,只需要檢查這個持有者標記即可。偏向鎖的優化在實際並沒有多執行緒競爭的場景下能夠有效提高程式的效能,但是當“沒有多執行緒競爭”這個假設不成立,偏向鎖就需要額外的邏輯進行撤銷,而這個撤銷就有可能會帶來較長時間的停頓,影響程式的效能。

為什麼說偏向鎖撤銷可能會導致長時間停頓呢,是因為偏向鎖的撤銷實際上需要在安全點時進行。偏向鎖撤銷的需要暫停擁有鎖的執行緒並操作它的棧,所以需要在安全點進行。而程式進入安全點所需的時間是不確定的,具體的原因就跟安全點的具體實現有關。

safepoint

安全點(以下稱safepoint)是jvm中的一個重要的概念,jvm中很多場景都會遇到它,最常見的應該是GC(雖然我前面提到的是偏向鎖撤銷)。safepoint的含義表示的是程式中的某些固定位置,在這些位置上程式的狀態是“確定”的,這時jvm就可以根據程式的狀態進行一些特殊的操作,比如:

  1. gc:gc時需要將不再存活的物件清理掉,所以需要“確定”地知道哪些物件不再存活。gc時還需要掃描每個執行緒的棧,所以需要“確定”的直到棧中的每個物件的型別(引用還是值)。
  2. 偏向鎖撤銷:這個前面提到了,因為需要暫停執行緒,同時操作執行緒的棧。
  3. 更新OopMap:oop(ordinary object pointer)是hotspot虛擬機器器裡用於記錄物件的元資料的資料結構,它記錄了物件內各個偏移量對應的資料的型別。OopMap主要用於實現準確式GC,關於準確式GC的介紹可以參考這裡
  4. Code deoptimization/Flushing code cache/Class redefinition/Various debug operation:來自這裡

safepoint的位置

在jvm的執行時管理中,利用safepoint來將整個程式掛起(Stop the World),然後進行一些特殊的操作。而safepoint的思路也很簡單:當我們需要執行一些特殊操作(比如gc)時,我們就在程式的某些位置設定一些暫停點,當執行緒來到這些暫停點時,就將自己掛起。等所有執行緒都掛起後,我們再進行原定的特殊操作。而safepoint的具體位置通常有以下幾個:

  1. 每個位元組碼命令之後(解釋模式)
  2. 所有的方法返回之前(JIT模式)
  3. 所有的非計數迴圈的末尾 (JIT模式)

只在非計數迴圈的末尾設定safepoint帶來了一個問題:假如程式裡有一個非常大的計數迴圈(比如迴圈100w次),就可能導致safepoint掛起整個程式的時間變長,因為其他已經掛起的執行緒都需要等待這個大迴圈執行結束。

前面提到safepoint的設定會使所有執行緒掛起,那麼具體的gc或者偏向鎖撤銷又是由誰來執行的呢,答案就是VM執行緒。在jstack的輸出結果中就可以看到有一個叫做"VMThread"的執行緒,這個執行緒就是專門負責在STW的時候處理各種特殊操作。

safe region

safepoint可以讓執行中的執行緒主動掛起,而其他狀態下的執行緒(比如正在sleep或者正阻塞在一個鎖上)則無法主動執行到safepoint。對於這種情況jvm中設定了安全區(safe region)的概念,當執行緒處於某些狀態時,jvm認為這種情況下這個執行緒不會對jvm heap做出任何修改,因此不會破壞jvm的“確定”的狀態,所以這些執行緒可以認為是安全的(處於安全區)。當處於安全區的執行緒要從安全區出來的時候,同樣需要檢查是否應該主動掛起。jvm中設定以下幾種狀態的執行緒就是處於safe region:

  1. 處於阻塞或者等待中
  2. 正在執行JNI方法

所以當執行緒處於以上幾個狀態時,我們就認為它們就和達到safepoint一樣,可以執行特殊的操作了。為了防止執行緒從safe region返回後對jvm heap進行更改,當STW時執行緒在從safe region返回時都會主動掛起。

執行緒掛起的實現

通過在特定的位置設定safepoint,我們可以讓程式在需要的時候掛起。當需要STW的時候,safepoint會被啟用,每個執行緒在執行到safepoint時,都會主動檢查safepoint的狀態,如果safepoint被啟用,執行緒就會進入掛起狀態。簡單來說,執行緒在檢查safepoint時會主動訪問一個特定的記憶體頁,而當STW時這個記憶體頁設定為不可讀,所以每個嘗試讀這個記憶體頁的執行緒也就會掛起。而這個檢查過程是通過JIT編譯器主動插入到指令中的。

而具體來說,執行緒處於不同狀態時,掛起執行緒的方式也有不同:

  1. 當執行緒處於解釋執行時,直譯器會強制將下一個指令指向檢查safepoint的指令
  2. 當執行緒正在執行JNI方法時,VMThread不會等待其返回,而是認為其處於safe region。當它返回時會阻塞直到STW結束
  3. 當執行緒正在執行已經編譯好的方法時,編譯好的程式碼裡會攜帶檢查safepoint的邏輯,我們將特定的記憶體頁設定為不可讀即可
  4. 當執行緒正在阻塞時,VMThread不會等待其返回,而是認為其處於safe region。當它返回時會阻塞直到STW結束

當執行緒嘗試訪問標記為不可讀的記憶體頁時,會觸發SIGSEGV訊號,從而觸發jvm內的signal handler,signal handler在收到SIGSEGV訊號時會確認是否是由於safepoint檢查觸發了這個訊號,如果是就會將自身掛起。

其他

很多內容參考了iter_zc的部落格,在此表示感謝。