天天看點

垃圾回收算法與Hotspot算法實作以及垃圾回收器垃圾回收算法 Hotspot的算法實作垃圾收集器

垃圾回收算法

垃圾回收算法與Hotspot算法實作以及垃圾回收器垃圾回收算法 Hotspot的算法實作垃圾收集器

1. 标記 - 清除

垃圾回收算法與Hotspot算法實作以及垃圾回收器垃圾回收算法 Hotspot的算法實作垃圾收集器

在标記階段,程式會檢查每個對象是否為活動對象,如果是活動對象,則程式會在對象頭部打上标記。

在清除階段,會進行對象回收并取消标志位,另外,還會判斷回收後的分塊與前一個空閑分塊是否連續,若連續,會合并這兩個分塊。回收對象就是把對象作為分塊,連接配接到被稱為 “空閑連結清單” 的單向連結清單,之後進行配置設定時隻需要周遊這個空閑連結清單,就可以找到分塊。

在配置設定時,程式會搜尋空閑連結清單尋找空間大于等于新對象大小 size 的塊 block。如果它找到的塊等于 size,會直接傳回這個分塊;如果找到的塊大于 size,會将塊分割成大小為 size 與 (block - size) 的兩部分,傳回大小為 size 的分塊,并把大小為 (block - size) 的塊傳回給空閑連結清單。

不足:

  • 标記和清除過程效率都不高;
  • 會産生大量不連續的記憶體碎片,導緻無法給大對象配置設定記憶體。

 2. 标記 - 整理

垃圾回收算法與Hotspot算法實作以及垃圾回收器垃圾回收算法 Hotspot的算法實作垃圾收集器

讓所有存活的對象都向一端移動,然後直接清理掉端邊界以外的記憶體。

優點:

  • 不會産生記憶體碎片

不足:

  • 需要移動大量對象,處理效率比較低。

3. 複制算法

垃圾回收算法與Hotspot算法實作以及垃圾回收器垃圾回收算法 Hotspot的算法實作垃圾收集器

将記憶體劃分為大小相等的兩塊,每次隻使用其中一塊,當這一塊記憶體用完了就将還存活的對象複制到另一塊上面,然後再把使用過的記憶體空間進行一次清理。

主要不足是隻使用了記憶體的一半。

現在的商業虛拟機都采用這種收集算法回收新生代,但是并不是劃分為大小相等的兩塊,而是一塊較大的 Eden 空間和兩塊較小的 Survivor 空間,每次使用 Eden 和其中一塊 Survivor。在回收時,将 Eden 和 Survivor 中還存活着的對象全部複制到另一塊 Survivor 上,最後清理 Eden 和使用過的那一塊 Survivor。

HotSpot 虛拟機的 Eden 和 Survivor 大小比例預設為 8:1,保證了記憶體的使用率達到 90%。如果每次回收有多于 10% 的對象存活,那麼一塊 Survivor 就不夠用了,此時需要依賴于老年代進行空間配置設定擔保,也就是借用老年代的空間存儲放不下的對象。

 效率:          複制算法>标記/整理算法>标記/清除算法(此處的效率隻是簡單的對比時間複雜度,實際情況不一定如此)。

 記憶體整齊度:複制算法=标記/整理算法>标記/清除算法。

 記憶體使用率:标記/整理算法=标記/清除算法>複制算法。

4. 分代收集

現在的商業虛拟機采用分代收集算法,它根據對象存活周期将記憶體劃分為幾塊,不同塊采用适當的收集算法。

一般将堆分為新生代和老年代。

  • 新生代使用:複制算法
  • 老年代使用:标記 - 清除 或者 标記 - 整理 算法

比如在新生代中,每次收集都會有大量對象死去,是以可以選擇複制算法,隻需要付出少量對象的複制成本就可以完成每次垃圾收集。而老年代的對象存活幾率是比較高的,而且沒有額外的空間對它進行配置設定擔保,是以我們必須選擇“标記-清除”或“标記-整理”算法進行垃圾收集。

空間配置設定擔保

在發生Minor GC之前,虛拟機會先檢查老年代最大可用的連續空間是否大于新生代所有對象總空間。如果這個條件成立,那麼Minor GC可以確定是安全的。如果不成立,則虛拟機會檢視HandlerPromotionFailure設定是否允許擔保失敗。如果允許,那麼會繼續檢查老年代最大可用的連續空間是否大于曆次晉升到老年代對象的平均大小。如果大于,将嘗試着進行一次Monitor GC,盡管這次GC是有風險的。如果小于,或者HandlerPromotionFailure設定不允許冒險,那這時也要改為進行一次Full GC了。

上述所說的冒險到底是冒的什麼險呢?

前面提到過,新生代使用複制收集算法,但是為了記憶體使用率。隻使用其中一個Survivor空間來作為輪換備份,是以當出現大量對象在Minor GC後仍然存活的情況(最極端的情況是記憶體回收之後,新生代中所有的對象都存活),就需要老年代進行配置設定擔保,把Survivor無法容納的對象直接進入老年代。老年代要進行這樣的擔保,前提是老年代本身還有容納這些對象的剩餘空間,一共有多少對象存活下來在實際完成記憶體回收之前是無法明确知道的,是以隻好取之前每一次回收晉升到老年代對象容量的平均大小值作為經驗值,與老年代的剩餘空間進行比較,決定是否進行Full GC來讓老年代騰出更多空間。

取平均值進行比較其實仍然是一種動态機率的手段,也就是說,如果某次Minor GC存活後的對象突增,遠遠高于平均值的話,依然會導緻擔保失敗。如果出現HandlerPromotionFailure失敗,那就隻好在失敗後重新發起一次FULL GC。雖然擔保失敗時繞的圈子是最大的,但大部分情況下都還是将HandlerPromotionFailure開關打開,避免Full GC過于頻繁。

JVM記憶體配置設定擔保機制

 Hotspot的算法實作

1.枚舉根節點

  在可達性分析中,可以作為GC Roots的節點有很多,但是現在很多應用僅僅方法區就有上百MB,如果逐個檢查的話,效率就會變得不可接受。

  而且,可達性分析必須在一個一緻性的快照中進行-即整個分析期間,系統就像當機了一樣。否則如果一邊分析,系統一邊動态表化,得到的結果就沒有準确性。這就導緻了系統GC時必須停頓所有的Java執行線程。

  目前主流Java虛拟機使用的都是準确式GC,是以當執行系統都停頓下來之後,并不需要一個不漏的檢查完所有執行上下文和全局的引用位置,虛拟機應該有辦法直接知道哪些地方存放着對象引用。在HotSpot實作中,使用一組稱為 OopMap 的資料結構來達到這個目的。OopMap會在類加載完成的時候,記錄對象内什麼偏移量上是什麼類型的資料,在JTI編譯過程中,也會在特定的位置記錄下棧和寄存器哪些位置是引用。這樣,在GC掃描的時候就可以直接得到這些資訊了。

2.安全點

  如果OopMap内容變化的指令非常多,HotSpot并不會為每條指令都産生OopMap,隻是在特定的位置記錄了這些資訊,這些位置成為“安全點”(SafePoint)。程式執行時隻有在達到安全點的時候才停頓開始GC。一般具有較長運作時間的指令才能被選為安全點,如方法調用、循環跳轉、異常跳轉等。

  接下來要考慮的便是,如何在GC時保證所有的線程都“跑”到安全點上停頓下來。這裡有兩種方案: 搶先式中斷 (Preemptive Suspension) 和主動式中斷 (Voluntary Suspension)。

  搶先式中斷會把所有線程中斷,如果某個線程不在安全點上,就恢複線程讓它跑到安全點上。幾乎沒有虛拟機采用這種方式。

  主動式中斷思想是需要中斷線程時,不直接對線程操作,而是設定一個GC标志,各個線程會輪詢這個标志并在需要時自己中斷挂起。這樣,輪詢标志的地方和安全點是重合的。

3.安全區域(Safe Region)

  安全點機制保證程式執行時,在不太長的時間内就會遇到可進入GC的安全點,但是,程式“不執行”的時候呢,程式不執行就是沒有配置設定CPU時間,典型的例子就是線程處于sleep或者blocked。這時候線程無法響應JVM的中斷請求,JVM顯然不太可能的等待線程重新被配置設定CPU時間。

  安全區域是指一段代碼片段之中,引用關系不會發生變化。在這個區域中的任意地方開始GC都是安全的。

  線上程執行到安全區域代碼時,首先辨別自己進入安全區域,當這段時間裡JVM發起GC,不用管辨別自己為安全區域的線程了。線上程要離開安全區域時,要檢查系統是否已經完成了根節點枚舉(或者整個GC過程),如果完成,線程繼續執行,否則等待直到收到可以安全離開安全區域的信号為止。

垃圾收集器

垃圾回收算法與Hotspot算法實作以及垃圾回收器垃圾回收算法 Hotspot的算法實作垃圾收集器

以上是 HotSpot 虛拟機中的 7 個垃圾收集器,連線表示垃圾收集器可以配合使用。

  • 單線程與多線程:單線程指的是垃圾收集器隻使用一個線程,而多線程使用多個線程;
  • 串行與并行:串行指的是垃圾收集器與使用者程式交替執行,這意味着在執行垃圾收集的時候需要停頓使用者程式;并行指的是垃圾收集器和使用者程式同時執行。除了 CMS 和 G1 之外,其它垃圾收集器都是以串行的方式執行。
  • 并行 :指多條垃圾收集線程并行工作,但此時使用者線程仍然處于等待狀态。
  • 并發:指使用者線程與垃圾收集線程同時執行(但不一定是并行,可能會交替執行),使用者程式在繼續運作,而垃圾收集器運作在另一個 CPU 上。 

1. Serial 收集器

新生代采用複制算法,老年代采用标記-整理算法

曾在jdk1.3.1之前是虛拟機新生代收集的唯一選擇

垃圾回收算法與Hotspot算法實作以及垃圾回收器垃圾回收算法 Hotspot的算法實作垃圾收集器

 在進行垃圾收集工作的時候必須暫停其他所有的工作線程( "Stop The World" ),直到它收集結束。

它的優點是簡單高效,在單個 CPU 環境下,由于沒有線程互動的開銷,是以擁有最高的單線程收集效率。

它是 Client 場景下的預設新生代收集器,因為在該場景下記憶體一般來說不會很大。它收集一兩百兆垃圾的停頓時間可以控制在一百多毫秒以内,隻要不是太頻繁,這點停頓時間是可以接受的。

2. ParNew 收集器

垃圾回收算法與Hotspot算法實作以及垃圾回收器垃圾回收算法 Hotspot的算法實作垃圾收集器
新生代采用複制算法,老年代采用标記-整理算法。 

ParNew 收集器其實就是 Serial 收集器的多線程版本,除了使用多線程進行垃圾收集外,其餘行為(控制參數、收集算法、回收政策等等)和 Serial 收集器完全一樣。

它是許多運作在 Server 模式下的虛拟機的首要選擇,除了 Serial 收集器外,隻有它能與 CMS 收集器(真正意義上的并發收集器)配合工作。

3. Parallel Scavenge 收集器

新生代采用複制算法,老年代采用标記-整理算法。 

Parallel Scavenge 收集器關注點是吞吐量(高效率的利用 CPU)。CMS 等垃圾收集器的關注點更多的是使用者線程的停頓時間(提高使用者體驗)。所謂吞吐量就是 CPU 中用于運作使用者代碼的時間與 CPU 總消耗時間的比值。

縮短停頓時間是以犧牲吞吐量和新生代空間來換取的:新生代空間變小,垃圾回收變得頻繁,導緻吞吐量下降。

4. Serial Old 收集器

垃圾回收算法與Hotspot算法實作以及垃圾回收器垃圾回收算法 Hotspot的算法實作垃圾收集器
新生代采用複制算法,老年代采用标記-整理算法。 

是 Serial 收集器的老年代版本,也是給 Client 場景下的虛拟機使用。如果用在 Server 場景下,它有兩大用途:

  • 在 JDK 1.5 以及之前版本(Parallel Old 誕生以前)中與 Parallel Scavenge 收集器搭配使用。
  • 作為 CMS 收集器的後備預案,在并發收集發生 Concurrent Mode Failure 時使用。

5. Parallel Old 收集器

垃圾回收算法與Hotspot算法實作以及垃圾回收器垃圾回收算法 Hotspot的算法實作垃圾收集器

是 Parallel Scavenge 收集器的老年代版本。

在注重吞吐量以及 CPU 資源敏感的場合,都可以優先考慮 Parallel Scavenge 加 Parallel Old 收集器。

6. CMS 收集器

垃圾回收算法與Hotspot算法實作以及垃圾回收器垃圾回收算法 Hotspot的算法實作垃圾收集器

CMS(Concurrent Mark Sweep)收集器是 HotSpot 虛拟機第一款真正意義上的并發收集器,它第一次實作了讓垃圾收集線程與使用者線程(基本上)同時工作。

CMS(Concurrent Mark Sweep),Mark Sweep 指的是标記 - 清除算法。

分為以下四個流程:

  • 初始标記:僅僅隻是标記一下 GC Roots 能直接關聯到的對象,速度很快,需要停頓。
  • 并發标記:進行 GC Roots Tracing 的過程,它在整個回收過程中耗時最長,不需要停頓。
  • 重新标記:為了修正并發标記期間因使用者程式繼續運作而導緻标記産生變動的那一部分對象的标記記錄,需要停頓。
  • 并發清除:不需要停頓。

在整個過程中耗時最長的并發标記和并發清除過程中,收集器線程都可以與使用者線程一起工作,不需要進行停頓。

具有以下缺點:

  • 吞吐量低:低停頓時間是以犧牲吞吐量為代價的,導緻 CPU 使用率不夠高。
  • 無法處理浮動垃圾,可能出現 Concurrent Mode Failure。浮動垃圾是指并發清除階段由于使用者線程繼續運作而産生的垃圾,這部分垃圾隻能到下一次 GC 時才能進行回收。由于浮動垃圾的存在,是以需要預留出一部分記憶體,意味着 CMS 收集不能像其它收集器那樣等待老年代快滿的時候再回收。如果預留的記憶體不夠存放浮動垃圾,就會出現 Concurrent Mode Failure,這時虛拟機将臨時啟用 Serial Old 來替代 CMS。
  • 标記 - 清除算法導緻的空間碎片,往往出現老年代空間剩餘,但無法找到足夠大連續空間來配置設定目前對象,不得不提前觸發一次 Full GC。

7. G1 收集器

G1(Garbage-First),它是一款面向服務端應用的垃圾收集器,在多 CPU 和大記憶體的場景下有很好的性能。HotSpot 開發團隊賦予它的使命是未來可以替換掉 CMS 收集器。

堆被分為新生代和老年代,其它收集器進行收集的範圍都是整個新生代或者老年代,而 G1 可以直接對新生代和老年代一起回收。

垃圾回收算法與Hotspot算法實作以及垃圾回收器垃圾回收算法 Hotspot的算法實作垃圾收集器

G1 把堆劃分成多個大小相等的獨立區域(Region),新生代和老年代不再實體隔離。

垃圾回收算法與Hotspot算法實作以及垃圾回收器垃圾回收算法 Hotspot的算法實作垃圾收集器

通過引入 Region 的概念,進而将原來的一整塊記憶體空間劃分成多個的小空間,使得每個小空間可以單獨進行垃圾回收。這種劃分方法帶來了很大的靈活性,使得可預測的停頓時間模型成為可能。通過記錄每個 Region 垃圾回收時間以及回收所獲得的空間(這兩個值是通過過去回收的經驗獲得),并維護一個優先清單,每次根據允許的收集時間,優先回收價值最大的 Region。

每個 Region 都有一個 Remembered Set,用來記錄該 Region 對象的引用對象所在的 Region。通過使用 Remembered Set,在做可達性分析的時候就可以避免全堆掃描。

垃圾回收算法與Hotspot算法實作以及垃圾回收器垃圾回收算法 Hotspot的算法實作垃圾收集器

如果不計算維護 Remembered Set 的操作,G1 收集器的運作大緻可劃分為以下幾個步驟:

  • 初始标記
  • 并發标記
  • 最終标記:為了修正在并發标記期間因使用者程式繼續運作而導緻标記産生變動的那一部分标記記錄,虛拟機将這段時間對象變化記錄線上程的 Remembered Set Logs 裡面,最終标記階段需要把 Remembered Set Logs 的資料合并到 Remembered Set 中。這階段需要停頓線程,但是可并行執行。
  • 篩選回收:首先對各個 Region 中的回收價值和成本進行排序,根據使用者所期望的 GC 停頓時間來制定回收計劃。此階段其實也可以做到與使用者程式一起并發執行,但是因為隻回收一部分 Region,時間是使用者可控制的,而且停頓使用者線程将大幅度提高收集效率。

具備如下特點:

  • 空間整合:整體來看是基于“标記 - 整理”算法實作的收集器,從局部(兩個 Region 之間)上來看是基于“複制”算法實作的,這意味着運作期間不會産生記憶體空間碎片。
  • 可預測的停頓:能讓使用者明确指定在一個長度為 M 毫秒的時間片段内,消耗在 GC 上的時間不得超過 N 毫秒。

參考連結:

CyC2018