HotSpot 虛擬機器垃圾回收演算法實現
作為使用範圍最廣的虛擬機器之一HotSpot,必須對垃圾回收演算法的執行效率有嚴格的考量,只有這樣才能保證虛擬機器高效執行
列舉根節點
從可達性分析中從 GC Roots 節點找引用鏈這個操作為例,可以作為 GC Roots 的節點主要在全域性性的引用(例如常量或者類靜態屬性)與執行上下文(例如棧幀中的本地變量表)中。
但是現在很多應用僅僅方法區就有數百兆,如果要逐個檢查這裡面的引用,那麼必然會消耗很多的時間。
另外,可達性分析對執行時間的敏感還體現在 GC 停頓上,因為這項分析工作必須在一個能確保一致性的快照中進行 —— 這裡的“一致性”指的是在整個分析過程中整個執行系統看起來就像是被凍結在某個時間點上,不可以出現分析過程中物件引用關係還在不停變化的情況,該點不滿足的話分析結果準確性就無法得到保證。
這點是導致 GC 進行時必須停頓所有 Java 執行執行緒的其中一個重要原因,即使是號稱不會發生停頓的 CMS 收集器中,列舉根節點也是必須要停頓的。
由於目前主流 Java 虛擬機器使用的都是準確式 GC,所以當執行系統停頓下來後,並不需要一個不漏的檢查完成所有執行上下文和全域性的引用位置,虛擬機器應當是有辦法直接得知那些地方存放著物件引用的。 在HotSpot的實現中,是使用一組被稱為OopMap 的資料結構來達到這個目的的,在類載入的時候,HotSpot就把物件內什麼偏移量上是什麼型別的資料計算出來,在JIT 編譯過程中,也會在特定的位置記錄下棧和暫存器中哪些位置是引用。這樣, GC 在掃描時就可以直接得知這些資訊了。
安全點
在 OopMap 的幫助下,HotSpot 可以快速並且準確的完成 GC Roots 列舉,但是一個很現實的問題隨之而來:可能導致引用關係變化,或者說 OopMap 內容變化的指令非常多,如果為每一條指令都生成對應的 OopMap,那麼將需要大量的額外空間,這樣 GC 的空間成本將會變得很高。
實際上,HotSpot 也的確沒有為每一條指令都生成 OopMap,只是在“特定的位置” 記錄了這些資訊,這些位置被稱為是安全點,即程式執行時並非是在所有地方都能停頓下來開始 GC ,只有到達安全點時才能暫停。
安全點的選擇既不能太少以至於讓 GC 等待太長時間,也不能過於頻繁以至於過分增大執行時負荷。
所以,安全點的選定基本上是以程式“是否具有讓程式長時間執行的特徵”為標準進行選定的——因為每條指令執行的時間都非常短暫,程式不太可能因為指令流長度太長這個原因而過長時間執行,“長時間執行”的最明顯特徵就是指令序列複用,例如方法呼叫、迴圈跳轉、異常跳轉等,所以具有這些功能的指令才會產生安全點。
對於安全點,另一個需要考慮的問題就是如何讓 GC 發生時讓所有執行緒(這裡不包括執行 JNI 呼叫的執行緒)都“跑”到最近的安全點再停頓下來。這裡有兩種方案可供選擇:搶先式中斷和主動式中斷。
- 搶先式中斷:無需執行緒的執行程式碼主動配合,在 GC 發生時,首先把所有執行緒全部中斷,如果發現有執行緒中斷的地方不再安全點上,就會發執行緒,讓它“跑”到安全點上。現在幾乎沒有虛擬機器實現採用搶先式中斷來暫停執行緒從而響應GC事件
- 主動式中斷:當 GC 需要中斷執行緒時,不直接對執行緒操作,僅僅簡單地設定一個標誌,各個執行緒執行時主動去輪詢這個標誌,發現中斷標誌為真時就自己中斷掛起。輪詢標誌的地方和安全點是重合的,另外再加上建立物件需要分配記憶體的地方。
安全區域
使用安全點似乎已經完美解決了如何進入 GC 的問題,但實際情況卻並不一定,安全點機制保證了程式執行,在不太長的時間內就會遇到可以進入 GC 的安全點。
但是執行緒“不執行”的時候呢?所謂不執行就是沒有分配 CPU 時間,典型的例子就是執行緒處於 Sleep 狀態或者 Blocked狀態,這時候執行緒無法響應 JVM 的中斷請求,“走”到安全點去中斷掛起,JVM顯然也不太可能等待執行緒重新被分配 CPU 時間。對於這種狀況,就需要安全區域來解決。
安全區域就是在一段程式碼片段中,引用關係不會發生變化,在這個區域中的任意地方開始 GC 都是安全的。
線上程執行到安全區域中的程式碼時,首先標識自己已經進入了安全區域,那樣,當這段時間裡 JVM 要發起 GC 時,就不用管標識自己為安全區域狀態的執行緒了。
當執行緒要離開安全區域時,它要檢查系統是否已經完成了根節點列舉(或者是整個 GC 過程),如果完成了,那執行緒就繼續執行,否則它就必須等待直到收到可以安全離開安全區域的訊號為止。
Linux公社的RSS地址 :https://www.linuxidc.com/rssFeed.aspx
本文永久更新連結地址:https://www.linuxidc.com/Linux/2019-02/156795.htm