作為使用范圍最廣的虛擬機之一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 過程),如果完成了,那線程就繼續執行,否則它就必須等待直到收到可以安全離開安全區域的信號為止。