深入理解java虛擬機——HotSpot的算法實現

1.枚舉根節點

檢查對象是否被引用需要根據GC Roots節點來查找引用鏈。可作爲GC Roots的節點主要是全局性的引用與執行上下文中,如果要逐個檢查引用,必然消耗時間。
另外可達性分析對執行時間的敏感還體現在GC停頓上,因爲這項分析工作必須在一個能確保一致性的快照中進行——這裏的“一致性”的意思是指整個分析期間整個系統執行系統看起來就行被凍結在某個時間點,不可以出現分析過程中對象引用關係還在不斷變化的情況,該點不滿足的話分析結果的準確性就無法得到保證。這點是導致GC進行時必須暫停所有Java執行線程的其中一個重要原因。

目前主流的java虛擬機使用的都是準確式GC,所以當執行系統停頓下來後,並不需要一個不漏地檢查完所有執行上下文和全局的引用位置,虛擬機應當是有辦法直接得到哪些地方存放着對象引用。在HotSpot的實現中,是使用一組成爲OopMap的數據結構來達到這個目的,在類加載完成的時候,HotSpot就把對象內什麼偏移量上是什麼類型的數據計算出來,在JIT編譯過程中,也會在特定的位置記錄下棧裏和寄存器裏哪些位置是引用。這樣GC在掃描時就就可以直接得知這些信息了。

對OopMap(Ordinary Object Pointer Map)的理解,引自http://dsxwjhf.iteye.com/blog/2201685

OopMap 用於枚舉 GC Roots ; 
RememberedSet 用於可達性分析。 

OopMap 

OopMap 記錄了棧上本地變量到堆上對象的引用關係。其作用是:垃圾收集時,收集線程會對棧上的內存進行掃描,看看哪些位置存儲了 Reference 類型。如果發現某個位置確實存的是 Reference 類型,就意味着它所引用的對象這一次不能被回收。但問題是,棧上的本地變量表裏面只有一部分數據是 Reference 類型的(它們是我們所需要的),那些非 Reference 類型的數據對我們而言毫無用處,但我們還是不得不對整個棧全部掃描一遍,這是對時間和資源的一種浪費。 
一個很自然的想法是,能不能用空間換時間,在某個時候把棧上代表引用的位置全部記錄下來,這樣到真正 gc 的時候就可以直接讀取,而不用再一點一點的掃描了。事實上,大部分主流的虛擬機也正是這麼做的,比如 HotSpot ,它使用一種叫做 OopMap 的數據結構來記錄這類信息。 
我們知道,一個線程意味着一個棧,一個棧由多個棧幀組成,一個棧幀對應着一個方法,一個方法裏面可能有多個安全點。 gc 發生時,程序首先運行到最近的一個安全點停下來,然後更新自己的 OopMap ,記下棧上哪些位置代表着引用。枚舉根節點時,遞歸遍歷每個棧幀的 OopMap ,通過棧中記錄的被引用對象的內存地址,即可找到這些對象( GC Roots )。 
通過上面的解釋,我們可以很清楚的看到使用 OopMap 可以避免全棧掃描,加快枚舉根節點的速度。但這並不是它的全部用意。它的另外一個更根本的作用是,可以幫助 HotSpot 實現準確式 GC (個人感覺這纔是 OopMap 被設計出來的根本原因,提高 GC Roots Enumeration 速度更像是一個“意外的驚喜”)。關於準確式 GC 的具體內容(如:什麼叫準確式 GC ?什麼叫保守式 GC ?什麼叫半保守式 GC ?準確式 GC 有哪些實現思路?等等),在此不一一說明,大家可以參考 找出棧上的指針/引用 這篇文章。需要說明的是,該文章的作者是 Oracle HotSpot 虛擬機團隊的開發人員。 

RememberedSet 

RememberedSet 用於處理這類問題:比如說,新生代 gc (它發生得非常頻繁)。一般來說, gc 過程是這樣的:首先枚舉根節點。根節點有可能在新生代中,也有可能在老年代中。這裏由於我們只想收集新生代(換句話說,不想收集老年代),所以沒有必要對位於老年代的 GC Roots 做全面的可達性分析。但問題是,確實可能存在位於老年代的某個 GC Root,它引用了新生代的某個對象,這個對象你是不能清除的。那怎麼辦呢? 

仍然是拿空間換時間的辦法。事實上,對於位於不同年代對象之間的引用關係,虛擬機會在程序運行過程中給記錄下來。對應上面所舉的例子,“老年代對象引用新生代對象”這種關係,會在引用關係發生時,在新生代邊上專門開闢一塊空間記錄下來,這就是 RememberedSet 。所以“新生代的 GC Roots ” + “ RememberedSet 存儲的內容”,纔是新生代收集時真正的 GC Roots 。然後就可以以此爲據,在新生代上做可達性分析,進行垃圾回收。 

我們知道, G1 收集器使用的是化整爲零的思想,把一塊大的內存劃分成很多個域( Region )。但問題是,難免有一個 Region 中的對象引用另一個 Region 中對象的情況。爲了達到可以以 Region 爲單位進行垃圾回收的目的, G1 收集器也使用了 RememberedSet 這種技術,在各個 Region 上記錄自家的對象被外面對象引用的情況。

2.安全點

爲了空間成本考慮,HotSpot沒有爲每條指令都生成OopMap,只是在特定的位置記錄了這些信息,這些位置稱爲安全點(Safepoint)。程序只有在到達安全點是才能停下來開始GC。安全點的設定基本上是以程序“是否具有讓程序長時間執行的特徵”爲標準進行選定的,長時間執行的最明顯特徵就是指令序列服用,例如 方法調用、循環跳轉、異常跳轉等。

如何在GC發生時讓所有的線程都到達安全點上停頓下來:搶先式中斷(Preemptive Suspension)和主動式中斷(Voluntary Suspension)。

搶先式中斷:不需要線程的執行代碼去主動配合,當發生 GC 時,先強制中斷所有線程,然後如果發現某些線程未處於安全點,那麼將其喚醒,直至其到達安全點再次將其中斷;這樣一直等待所有線程都在安全點後開始 GC。

主動式中斷:不強制中斷線程,只是簡單地設置一箇中斷標記,各個線程在執行時輪詢這個標記,一旦發現標記被改變(出現中斷標記)時,那麼將運行到安全點後自己中斷掛起;目前所有商用虛擬機全部採用主動式中斷。

3.安全區域

Safepoint機制保證了程序執行時,在不太長的時間會進入GC的Safepoint,但如果線程處於Sleep狀態或者Blocked狀態,這種情況需要安全區域(Safe Region)來解決。

安全區域是指在一段區域內,對象引用關係等不會發生變化,在此區域內任意位置開始 GC 都是安全的;線程運行時,首先標記自己進入了安全區,然後在這段區域內,如果線程發生了阻塞、休眠等操作,JVM 發起 GC 時將忽略這些處於安全區的線程。當線程再次被喚醒時,首先他會檢查是否完成了 GC Roots枚舉(或這個GC過程),然後選擇是否繼續執行,否則將繼續等待 GC 的完成。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章