JVM垃圾收集(二)垃圾收集算法

1 標記-清除算法(Mark-Sweep)

這是最基礎的算法,這個算法分爲兩個階段標記和清除。首先標記出所有需要清除的對象,然後統一回收標記的對象。這種算法是最簡單的,後續的算法都是在它的基礎上改進得到的。它存在兩個問題:

  • 它的標記和清除效率都不高
  • 是空間問題,標記清除後,產生大量的不連續的內存碎片。這可能導致在以後的對象分配的過程中,需要佔用空間較大的對象可能找不到合適的連續內存空間而分配失敗,觸發另一次內存收集

2 複製算法

這種算法把內存按容量劃分爲相等兩個區域,每次只使用其中的一塊。當一塊內從用完了,就把還存活的對象複製到另外一個內存上,然後把整塊內存清除掉。這樣每次都是對整個半區進行回收,在分配內存的時候也不用考慮內存碎片的問題,只要移動堆頂指針按順序分配就可以了,實現簡單。

現在商業的虛擬機都使用這種方式回收新生代。研究表明,98%的新生代對象都是很快消亡的,所有不需要按照1:1來劃分空間,而是將內存分爲一塊較大的Eden區,和兩個較小的Survivor空間,每次使用Eden和其中的一塊survivor空間。當回收時,將Eden和survior空間上還存活的對象放入另一塊survivor空間,清理掉Eden和剛剛使用的survivor區域。HotSpot區域默認的 Eden與survivor的大小比爲8:1,也就是新生代每次佔用90%的新生代空間進行內存分配,只有10%被浪費掉。當回收複製時,survivor空間不足時,就要依賴老年代進行分配擔保。

3 標記-整理算法

複製算法如果在對象存活率較高的情況下就要進行較多的複製操作,效率會變得很低。特別對應對象100%存活的極端情況,所以在老年代中,不宜採用複製算法。根據老年代的特點,提出了“標記-整理”算法。它的標記過程是跟前面的標記算法一樣的,但是不是對可回收對象直接進行回收,而是讓所有存活的對象都向一端移動,然後直接清理掉邊界意外的內存空間。

4 分代收集

目前虛擬機都採用分代收集的算法,把java堆分爲新生代和老年代。新生代中對象消亡的比較快,採用複製算法,老年代中對象存活率較高,採用標記-清除或標記整理進行回收

5 HotSpot的算法實現

5.1 枚舉根節點

由前面可知,確定對象是否存活源於GCROOT鏈。可以作爲GCROOts的節點主要在全局性的引用(例如常量或靜態屬性)與執行上下文(如棧幀中的本地變量表)中,然後這種遍歷式地檢查引用會消耗很多時間。

另外這個過程是對時間敏感的,需要保證這個檢查結果建立在GC停頓上,也就是這項分析工作在一個確保一致性的快照中進行。就好像系統凍結在某一時刻一樣,不能隨着分析進行,對象的引用關係還在發生變化,這樣就無法保證準確性。正是因爲這點的考慮,GC必須停頓Java的所有線程(stop-the -world),及時在CMS收集器中,這個GC停頓也是必須的。

爲了能夠快速的在系統停頓後檢查引用關係,在HotSpot中實現了一個OopMap的數據結構來達到這個目的,在類加載完成的時候,HotSpot就把對象內什麼偏移量上是什麼類型的的數據計算出來,在JFT編譯的時候,也會在特定的位置記錄下棧和寄存器中哪些爲位置是引用。這樣GC掃描就可以直接得到這種信息

5.2 安全點

前面提到在特定的位置記錄下引用信息放入OopMap中,這個特定的位置就是安全點。程序只有到達安全點的時候,纔可以發生GC停頓。安全點的選擇要合理,太少可能導致GC停頓時間過長,太多將會導致GC停頓頻繁發生,一般來講在方法調用、循環跳轉、異常跳轉時,才產生安全點

 

還有一個問題就是,如何讓GC發生時,所有程序都“跑到”安全點停頓下來。有兩種解決方案

  • 搶先式中斷,GC發生時,所有線程全部中斷。如果發生有線程不在安全點上,就恢復線程,讓它跑到安全點停頓下來,現在機會沒有虛擬機採用這種方式
  • 主動式中斷,爲每個線程設置一個標誌,線程主要去輪詢這個標誌,標誌爲真時把自己掛起,這個標誌就是與安全點重合的位置。這樣保證了掛起的時刻永遠在安全點。

5.3 安全區域

安全點似乎解決了程序如何進入GC的問題,但是這隻針對於“跑着”的線程是有用的,如果線程本身已經處於sleep或者blocked狀態,這個線程就無法響應JVM中斷請求,這時就需要安全區

安全區是指一段代碼中,引用關係不會發生變化的區域。在這個區域中開始GC是安全的。當線程代碼進入到安全區以後,這段時間內發生的GC就不管進入安全區的線程。當線程想要離開安全區的時候,要檢查系統是否處於GC狀態。如果在發生GC,就要等待系統發生可以離開安全區的信號。


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