Java虛擬機03——垃圾收集算法

這裏簡要介紹幾種垃圾收集算法的思想算法

標記 - 清除算法

該算法如同它的名字同樣,分爲「標記」和「清除」兩個階段:安全

  • 首先標記出全部須要回收的對象
  • 在標記完後統一回收全部被標記的對象

image.png

這個算法其實已通過時了,可是後續的算法都是基於這種思路來的。它主要的不足點有兩個:數據結構

  1. 效率問題。標記和清理兩個過程的效率都不高
  2. 空間問題。標記清除後會產生大量不連續的內存碎片,空間碎片太對會致使程序運行過程當中須要分配大對象時,沒法找到連續的內存而不得不提早觸發另外一次垃圾收集動做

複製算法

複製算法的流程以下:線程

  • 它將可用內存按容量大小劃分爲大小相等的兩塊,每次只使用其中一塊。
  • 當這塊的內存用完了,就將還存活着的對象複製到另外一塊上面,
  • 而後把使用過的內存空間一次性清理掉。

image.png

能夠看到每次只對一半區域進行收集,這樣就不用考慮內存碎片等複雜狀況了,只要移動堆頂指針,按順序分配內存便可,實現簡單,運行高效。可是這種算法的代價是將內存縮小爲原來的一半,內存成本高指針

複製算法通常用於收集新生代,由於新生代大部分的對象的存活時間很短,所以新生代中存活的對象遠遠少於垃圾對象。cdn

新生代:存放年輕對象的堆空間。年輕對象是指剛剛建立,或者經歷垃圾回收次數很少的對象。
老年代:存放老年對象的堆空間。老年對象指經歷過屢次垃圾回收依然存活的對象。對象

在商業虛擬機中,例如咱們常見的HotSpot虛擬機,將新生代分爲一個Eden區和兩個Survivor區,Eden區與Survivor區的大小比例是8:1,也便是說Eden區佔新生代的80%,兩個Survivor分別佔10%。新生代的複製算法執行規則以下:blog

  • 每次使用複製算法進行垃圾回收時,會將Eden區和其中一塊Survivor區的全部存活對象複製到另外一塊空閒Survivor區中,在複製操做中,大對象和老年對象將直接複製到老年代;
  • 而後將原來的Eden區和Survivor區的對象一次性清理掉;
  • 若是在執行復制算法時一塊空閒Survivor區域不可以容納原來的Eden區和Survivor區的對象,就須要依賴老年代,將多餘的對象直接複製到老年代。

能夠發現,這種複製機制保證只有一塊Survivor區的內存(僅佔新生代內存的10%)是被浪費的。新生代的複製算法示意圖以下:事件

image.png

標記 - 整理算法

在對象存活率較低的新生代使用複製算法效率高。那麼在對象存活率高的老年代,使用複製算法效率將會變得很低。根據老年代的特色,有人提出了「標記 - 整理」算法。算法流程以下:內存

  • 首先標記出全部須要回收的對象
  • 讓全部存活的對象都向一端移動
  • 而後清理掉端邊界之外的內存

image.png

分代收集算法

當前商業虛擬機的垃圾收集算法都採用「分代收集算法」。主要思想是根據對象存活週期的不一樣將內存劃分爲幾塊,並採用最適合的收集算法。

  • 在大批對象死去,少許存活的新生代中,採用複製算法
  • 在對象存活率高、沒有額外空間對它進行分配擔保,採用「標記 - 清理」或「標記 - 整理」算法。

OopMap、Safe Point和Safe Region

上面介紹了幾種垃圾收集算法,可是虛擬機(這裏以HotSpot爲例子)在發起內存回收的時候會遇到不少問題。所以誕生了OopMap、Safe Point和Safe Region來解決

OopMap

問題:

  • GC Roots的節點主要在全局性的引用(例如常量或類靜態屬性)與執行上下文(例如棧幀中的本地變量表)中,可是如今引用衆多,若是要逐個檢查這裏面的引用,那麼必然會消耗不少時間。
  • 另外,可達性分析對執行時間的敏感還體如今GC停頓上,由於這項分析工做必須在一個能確保一致性的快照中進行————這裏「一致性」是指分析過程當中不能夠出現引用關係還在不斷變化的狀況,所以GC進行時必須停頓全部的Java執行線程

解決: 在HotSpot的實現中,使用一組成爲OopMap的數據結構。

  • 在類加載完成的時候,就把對象內什麼偏移量上是什麼類型的數據計算出來
  • 在JIT編譯過程當中,也會在特定位置記錄下棧和寄存器中哪些位置是引用

這樣,GC在掃描時就能夠直接得知這些信息了。

Safe Point

在OopMap的協助下,HotSpot能夠快速且準確地完成GC Roots枚舉,但一個很現實的問題隨之而來:

  • OopMap內容變化的指令過多致使須要大量額外空間的問題

解決:

  • HotSpot沒有爲每條指令都生成OopMap,只是在「特定的位置」記錄了這些信息,這些位置稱爲安全點(Safe Point),即程序執行時只有在到達Safe Point時才能更新本身的OopMap。

對於Safe Point,另外一個須要考慮的問題是如何在GC發生時讓全部線程(這裏不包括執行JNI調用的線程)都運行到最近的安全點上再停頓下來。這裏有兩種方案可供選擇:搶先式中斷(Preemptive Suspension)和主動式中斷(Voluntary Suspension):

  • 搶先式中斷。不須要線程的執行代碼主動去配合,在GC發生時,首先把全部線程所有中斷,若是發現有線程中斷的地方不在安全點上,就恢復線程,讓它繼續運行到安全點上。如今幾乎沒有虛擬機實現採用搶先式中斷來暫停線程從而響應GC事件。
  • 主動式中斷。當GC須要中斷線程的時候,不直接對線程操做,僅僅簡單地設置一個標誌,各個線程執行時主動去輪詢這個標誌,發現中斷標誌爲真時就本身中斷掛起。輪詢標誌的地方和安全點是重合的,另外再加上建立對象須要分配內存的地方。

Safe Region

Safe Point機制保證了程序執行時,在不太長的時間內就會遇到可進入GC的Safe Point。 問題:

  • 可是當線程沒有分配CPU時間(如線程處於Sleep狀態或者Blocked狀態),這時候線程沒法響應JVM的中斷請求以繼續到安全的地方去中斷掛起,JVM也顯然不太可能等待線程從新被分配CPU時間。對於這種狀況,就須要安全區域(Safe Region)來解決。

安全區域是指在一段代碼片斷之中,引用關係不會發生變化。在這個區域中的任意地方開始GC都是安全的。咱們也能夠把Safe Region看作是被擴展了的Safepoint。

在線程執行到Safe Region中的代碼時,首先標識本身已經進入了Safe Region,當在這段時間裏JVM要發起GC時,就不用管標識本身爲Safe Region狀態的線程了。在線程要離開Safe Region時,它要檢查系統是否已經完成了根節點枚舉(或者是整個GC過程),若是完成了,那線程就繼續執行,不然它就必須等待直到收到能夠安全離開Safe Region的信號爲止。

相關文章
相關標籤/搜索