虛擬機研究系列-「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

參考文獻


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM