1. 程式人生 > 其它 >虛擬機器研究系列-「GC本質底層機制」SafePoint的深入分析和底層原理探究指南

虛擬機器研究系列-「GC本質底層機制」SafePoint的深入分析和底層原理探究指南

SafePoint前提介紹

在高度優化的現代JVM裡,Safepoint有幾種不同的用法。GC safepoint是最常見、大家聽說得最多的,但還有deoptimization safepoint也很重要。

在HotSpot VM裡,這兩種Safepoint目前實現在一起,但其實概念上它們倆沒有直接聯絡,需要的資料不一樣。

無論是哪種SafePoint,最簡潔的定義是“A point in program where the state of execution is known by the VM”。這裡“state of execution”特意說得模糊,是因為不同種類的safepoint需要的資料不一樣。

GC safepoint

GC Safepoint需要知道在那個程式位置上,呼叫棧、暫存器等一些重要的資料區域裡什麼地方包含了GC管理的指標; 如果要觸發一次GC,那麼JVM裡的所有Java執行緒都必須到達GC safepoint。

Deoptimization safepoint

Deoptimization safepoint需要知道在那個程式位置上,原本抽象概念上的JVM的執行狀態(所有區域性變數、臨時變數、鎖,等等)到底分配到了什麼地方,是在棧幀的具體某個運算元棧slot,還是在某個暫存器裡。

如果要執行一次deoptimization,那麼需要執行deoptimization的執行緒要在到達deoptimization safepoint之後才可以開始deoptimize。

不同JVM實現會選用不同的位置放置safepoint。

HotSpotVM的SafePoint

直譯器裡每條位元組碼的邊界都可以是一個safepoint,因為HotSpot的直譯器總是能很容易的找出完整的“state of execution”。

JIT編譯的世界裡,HotSpot會在所有方法的臨返回之前,以及所有非counted loop的迴圈的回跳之前放置safepoint,(counted loop則沒有放置safepoint)。

HotSpot的JIT編譯器不但會生成機器碼,還會額外在每個safepoint生成一些“除錯符號資訊”,以便VM能找到所需的“state of execution”。

SafePoint的儲存資訊

為GC SafePoint生成的符號資訊是OopMap,指出棧上和暫存器裡哪裡有GC管理的指標;

為deoptimization SafePoint生成的符號資訊是debugInfo,指出如果要把當前棧幀從compiled frame轉換為interpreted frame的話,要從哪裡把相應的區域性變數、臨時變數、鎖等資訊找出來。

選擇在SafePoint的位置地點

  • 掛在safepoint的除錯符號資訊要佔用空間,如果允許每條機器碼都可以是safepoint的話,需要儲存的資料量會很大(當然這有辦法解決,例如用delta儲存和用壓縮)

  • safepoint會影響優化,特別是deoptimization safepoint,會迫使JVM保留一些只有直譯器可能需要的、JIT編譯器認定無用的變數的值。本來JIT編譯器可能可以發現某些值不需要而消除它們對應的運算,如果在safepoint需要這些值的話那就只好保留了。這才是更重要的地方,所以要儘量少放置safepoint。

像HotSpotVM這樣,在Safepoint會生成(polling程式碼)主動請求詢問JVM是否要進入safepoint,polling也有開銷所以要儘量減少。

Native程式碼的特殊性

當某個執行緒在執行native函式的時候。此時該執行緒在執行JVM管理之外的程式碼,不能對JVM的執行狀態做任何修改,因而JVM要進入safepoint不需要關心它。

所以也可以把正在執行native函式的執行緒看作“已經進入了safepoint”,或者把這種情況叫做“在safe-region裡”。

JVM外部要對JVM執行狀態做修改必須要通過JNI。所有能修改JVM執行狀態的JNI函式在入口處都有safepoint檢查,一旦JVM已經發出通知說此時應該已經到達safepoint就會在這些檢查的地方停下來把控制權交給JVM。

JRockit選擇放置safepoint的地方在方法的入口以及迴圈末尾回跳之前,跟HotSpot略為不同。

UseCountedLoopSafepoints:

可以避免GC發生時,執行緒因長時間執行counted loop,進入不到safepoint,而引起GC的STW時間過長。

JVM引數-XX:+PrintSafepointStatistics -XX:PrintSafepointStatisticsCount=1

在控制檯輸出以下資訊:

vmop [threads: total initially_running wait_to_block]  [time: spin block sync cleanup vmop] page_trap_count  370337.312: GenCollectForAllocation     [  1070     2       3  ]   [ 8830   0 8831   1  24  ] 

YGC所花費的時間非常短,主要時間花費在所有執行緒達到安全點並暫停。

JVM引數配置如下:

-server -Xms8192M -Xmx8192M -Xmn1500M -Xss256k -XX:PermSize=256M -XX:MaxPermSize=256M -XX:SurvivorRatio=8 -XX:-UseBiasedLocking -XX:MonitorBound=16384 -XX:+UseSpinning -XX:PreBlockSpin=1 -XX:+UseParNewGC -XX:ParallelGCThreads=8 -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=55 -XX:CMSMaxAbortablePrecleanTime=5 -XX:+CMSParallelRemarkEnabled -XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=0 -XX:+CMSClassUnloadingEnabled -XX:+UseFastAccessorMethods -XX:+UseCMSInitiatingOccupancyOnly -XX:SoftRefLRUPolicyMSPerMB=0 -XX:+DisableExplicitGC -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/home/xmail/jvm_heap.dump -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCApplicationStoppedTime -XX:+PrintGCApplicationConcurrentTime -XX:+PrintSafepointStatistics -XX:PrintSafepointStatisticsCount=1  

最有可能導致問題的是程式碼裡有Java程式碼

for (int i = 0; i < ...; i++) { } 或者類似的迴圈程式碼。

這種迴圈稱為“counted loop”,就是有明確的迴圈計數器變數,而且該變數有明確的起始值、終止值、步進長度的迴圈。

它有可能被優化為迴圈末尾沒有safepoint,於是如果這個迴圈的迴圈次數很多、迴圈體裡又不呼叫別的方法或者是呼叫了方法但被內聯進來了,就有可能會導致進入safepoint非常耗時。

可惜的是現在沒什麼特別方便的辦法直接指出是什麼地方有這種迴圈。有的話,一種解決辦法是把單層迴圈拆成等價的雙重巢狀迴圈,這樣其中一層迴圈末尾的safepoint就可能會留下來,減少進入safepoint的等待時間。

如何判斷內聯方法

從程式碼角度如何判斷方法被內聯進來了,主要是方法被final修飾。 final是可以幫助JIT編譯器做出內聯的判斷,但不是必要條件。

  • -XX:+PrintCompilation -XX:+PrintInlining來看內聯狀況

  • -XX:+PrintSafepointStatistics -XX:PrintSafepointStatisticsCount=1 ”輸出的結果“[time: spin block sync cleanup vmop] ”中spin是指什麼呢?

    • PrintSafepointStatics:打印出來的spin值指的是SafepointSynchronize在同步每個執行緒時做的自旋。

thread locking / biased locking的spin完全沒關係,自然設定那些引數也不會影響safepoint的自旋(UseSpinning之類控制的是thread locking的自旋)。

SafePoint存在的目的?

為什麼把這些位置設定為jvm的安全點呢,主要目的就是避免程式長時間無法進入safepoint,比如JVM在做GC之前要等所有的應用執行緒進入到安全點後VM執行緒才能分派GC任務 ,如果有執行緒一直沒有進入到安全點,就會導致GC時JVM停頓時間延長。

比如,寫了一個超大的迴圈導致執行緒一直沒有進入到安全點,GC前停頓了8秒。

產生的日誌資訊基本上STW的原因都是RevokeBias或者BulkRevokeBias。這個是撤銷偏向鎖操作,雖然每次暫停的時間很短,但是特別頻繁出現也會很耗時。

GC如何找到不可用的物件

編寫程式碼的時候是可以知道物件不可用的,但對於程式來說,需要一定的方式來知曉,可用方法比如:編譯分析,引用計數,和物件是否可達。

可達性分析

因而可達性分析,只需要找到直接可達的引用,直接可達的引用就是根引用,根引用的集合就是根的集合

  1. 一個物件只要能夠通過mutator觸達,那麼它就是“活”著的。

  2. 如果Mutator棧的一個槽位包含了物件的引用,那麼物件就是直接可觸達。

  3. 從直接可達物件可觸達的物件必定也是可達的,

muator執行緒分析
  • mutator的上下文就包含了直接可達的資料,所以要獲取物件根集合就是要找到mutator上下文中的物件引用,

  • mutator的上下文指的就是它的棧、它的暫存器檔案以及一些執行緒上特定的資料。

靜態資料

全域性資料本身也是直接可達的

可達性分析為了確保能正確的決定物件是否存活,GC需要獲取mutator 上下文的(當前)一致性快照,然後列舉所有的根物件。

  • 一致性指的是:快照的抽取就像只在一個時間點發生,來避免丟失一些活著的物件。
如何獲取 mutator上下文的一致性快照

一種簡單的方式就是在跟引用的過程中暫停所有的執行緒。當mutator暫停了它的執行時,只有將所有引用資訊儲存在其上下文中,才能列舉根的集合,這意味著,mutator需要能夠告訴JVM哪些棧的槽位有用,哪些暫存器持有引用。

如果GC能夠準確的獲取上述引用資訊,它就稱作精準根集合列舉。而無法獲取就是不精準的。

如何獲取精準的引用資訊列舉

對於java來說,JIT知曉所有的棧幀資訊和暫存器的內容,當JIT編譯一個方法時,對於每條指令,它都可以去儲存根引用資訊,儲存意味著額外的儲存空間,如果要儲存所有的指令就顯得花銷太大,另外在真實的執行過程中也只有少數指令才會成為暫停點,因此JIT只需要儲存這些指令點的資訊就夠了。而真正有機會成為暫停點的地方就稱作 safe-points,即能夠安全的列舉根集合的暫停點

如何保證mutator會在safe-point暫停

當GC想要觸發一次回收時,它會設定一個標誌,mutator則週期性的去檢查(poll)這個標誌,如果檢查到了,就會立馬暫停,這裡的檢查點(poll points)也是安全點,由JIT負責把poll points放到合適的位置,那些地方適合設定檢查GC事件的標記

polling point插入的主要原則是:
  • polling point應該足夠多,防止GC等一個mutator的暫停太長,導致其他mutator都走在等GC釋放空間,程式整個等待過長

  • polling point不能太頻繁導致執行時儲存開銷過大

  • polling本身也是有開銷的,不能過多

  • 權衡下來只在必須和必要的地方加

  • 分配地址的時候強制新增,因為分配空間很有肯能導致回收,所以這裡是一個安全點

  • 長時間的執行一般意味著迴圈和方法呼叫,所以方法呼叫和迴圈返回最好加上

但是有時候並不是長時間的執行,而是長時間的空閒,比如 sleep、block,執行緒在執行其他的native函式,這些時候JVM無法掌控執行能力,也就無法響應GC事件。

SafePoint無法解決sleep/block 帶來的問題,當這段時間內JVM要發起GC時,就不管沒到安全點但是在安全區域的執行緒。線上程要離開安全區域時,要檢查系統是否已經完成了GC,故我們又定義了一個安全區域的概念.

SafeRegion的簡介

safe-region是指程式碼快中沒有用到會變異的部分,這樣的程式碼塊中,任何一個點都可以安全的列舉根。

  • 當進入到safe-region中時,mutator會設定一個準備標記,在離開safe-region區域之前,會檢查GC是否已經完成了回收,如果沒有,那麼就暫停執行,如果有,就可以直接離開safe-region區域,不需要暫停mutator。

  • 關於Java/JVM的safepoint / safe-region,程式碼的執行過程中,如果需要執行某些操作,比如GC,deoptimize,等等,必須知道當前程式所有執行緒執行到的地方,是否能夠恰好滿足我執行對應操作,而不會對應用程式本身造成損害,能夠正確執行的地方就是safepoint/saferegion

參考文獻

極限就是為了超越而存在的