通俗易懂 JVM 中的 GC 實現

在上篇文章中介紹了Java GC:基礎原理,這篇文章來看下在 JVM 中是如何實現具體的收集器的。git

JVM 提供了多種垃圾收集器用來分別收集新生代和老年代,新老收集器之間能夠組合使用,可是在實際使用中(基於Java 8),通常常見的有4種收集器組合github

  • Serial GC 收集新生代和老年代 -XX:+UseSerialGC
  • Parallel GC 收集新生代和老年代(JVM默認使用的組合) -XX:+UseParallelGC(該參數包含了-XX:+UseParallelOldGC)
  • Parallel New 收集新生代 + Concurrent Mark and Sweep (CMS) 收集老年代 -XX:+UseConcMarkSweepGC(該參數包含了 -XX:+UseParNewGC)
  • G1 (不嚴格區分新老代)-XX:+UseG1GC-

PS: 在 Java 8 中 -XX:+UseParallelOldGC 和 -XX:+UseParNewGC 已經沒法單獨使用算法

下面咱們從原理上去理解每一個收集器是如何工做的,但並不會去細究具體的實現。數據結構

Serial GC

Serial GC 使用 mark-copy 算法處理新生代,使用 mark-sweep-compact 算法處理老年代。正如名字所說的那樣,這是一個單線程的收集器。整個收集過程會觸發 stop-the-world 暫停應用,直到回收結束。該收集器適用於只有幾百M大小的堆和單核 CPU的狀況。對於服務端,不多會使用這個組合,由於沒法合理的使用計算機資源,不過反過來講的話,這也能夠知足對系統資源使用有限制的狀況。多線程

Parallel GC

Parallel GC 和 Serial GC 使用的算法同樣。只不過是多線程的,是 JVM 默認使用的 GC 。和 Serial GC 同樣,在整個收集過程會觸發 stop-the-world 暫停應用,直到回收結束。在多核環境下,處理速度要比 Serial GC 快,能夠提升吞吐量。併發

Concurrent Mark and Sweep(CMS)

Serial GC 和 Parallel GC 在回收垃圾的過程當中須要暫停應用線程,所以若是對應用延遲有要求的話,CMS 收集器是一個更好的選擇。其使用 stop-the-world 的 mark-copy 算法收集新生代(ParNew,和 Parallel GC 類似但不兼容,只和 CMS 一塊兒使用),使用幾乎和應用線程併發的 mark-sweep 算法收集老年代。post

PS :至於 ParNew 和 Parallel 的關係是有歷史緣由的,有興趣的能夠看這個爲何有Parallel GC 了,還要有個 ParNew,還不兼容?線程

CMS 主要用於避免老年代回收時長時間的暫停。首先,其使用了 mark-sweep 算法而不進行壓縮(注意因爲會產生內存碎片,所以在內存分配上使用了 free-lists 而不是指針碰撞,因此分配速度會相對慢一些) 。其次,在 mark-sweep 算法收集過程當中,與應用程序幾乎是併發的,幾乎不影響應用程序的執行(暫停的時間很短)。設計

PS:CMS爲何沒有采用標記-整理算法來實現?3d

不過凡事有利即有弊

  • 因爲大多數時候 CMS 至少使用了一些 CPU 資源而沒有執行應用程序的代碼,所以 CMS 的吞吐量一般要比 Parallel GC 差。
  • 因爲 sweep 後沒有進行 compact,必然會形成內存碎片的問題(JVM 提供了參數能夠設置在一次或屢次 Full GC 後對內存進行整理)。
  • 因爲存在和應用程序併發的階段,不能和其餘收集器同樣,等到老年代放不下了才作回收,須要設定一個閾值。若是在併發過程當中,老年代滿了的話,則會觸發 「Concurrent Mode Failure」 錯誤,說明併發過程失敗,會 stop-the-world,執行 Full GC,等到回收完畢後才恢復應用線程。
  • 會存在浮動垃圾,即標記存活後死亡的對象,須要在下一次 GC 中才能清理。

CMS 的併發收集過程以下:

  1. 初始標記(Initial Mark) 初始標記是一個 stop-the-world 的過程,不過該過程只須要標記老年代中全部 GC Roots 可直接抵達的對象,時間很是短。
  2. 併發標記(Concurrent Mark) 在不暫停用戶線程的狀況下,併發的標記存活對象。此時因爲應用線程還在運行,可能會有一些新的對象進入到老年代,或者以前的對象在標記後發生一些引用的變化。在以前介紹的基礎算法中的內容提到過 JVM 使用了 Card Marking 的技術來標記老年代對新生代的引用;這裏也使用了相同的技術,併發過程當中產生的引用變化也使用了一個和 CardTable 相似的結構(ModUnionTable)記錄下引用變化的對象。
  3. 預清理階段 (Concurrent Preclean) 這個階段須要處理上個階段遺留的問題,遍歷 Ditry Card 中對象從新標記一遍存活對象。不過,對於以前標記存活但如今已經死亡的垃圾對象就須要留到下一次清理了(浮動垃圾)。這個階段是能夠關閉的。
  4. 可中斷預清理階段 (Concurrent Abortable Preclean) 重複執行上一個階段的操做,直到知足某種條件,能夠是循環次數達到閾值或者循環處理時間達到閾值等等。該階段的目的在於儘量的去處理那些在併發階段被應用線程更新的老年代對象,以減小從新標記階段的暫停時間。
  5. 最終從新標記 (Final Remark). 只要還在併發,就永遠存在新的引用關係沒有處理,所以須要整個過程當中第二個也是最後一個 stop-the-world 的階段來確保全部存活的對象都被標記。
  6. 併發清除 (Concurrent Sweep) 因爲不須要 compact,沒有 stop-the-world 的必要,直接清除就行了。CMS會維護本身的空閒列表,不會把在這個階段中新晉升上來的對象給清除了。
  7. 併發重置 (Concurrent Reset) 重置內部的數據結構以便下一次使用

此外,除了被動的由 Mionr GC觸發 Old GC 外,在實際應用中,還存在着主動的,週期性的 Old GC,只不過存在着觸發條件,有興趣的能夠自行額外查閱。

雖然 JDK 9 已經廢除 CMS 垃圾收集器,但 CMS 仍然是 JDK 8 及之前的追求低延遲的不錯的可選項 。

G1 - Garbage First

爲解決 CMS 算法產生的一系列問題缺陷,HotSpot提供了另一種垃圾回收策略,從 JDK 9 開始,G1 取代 Parallel GC 成爲 JVM 中的默認 GC。G1 的其中一個核心目標是使 stop-the-world 的時間可預測而且可配置。好比說,你能夠要求每分鐘 stop-the-world 的暫停時間不能超過 5 毫秒,G1 會盡最大可能去完成需求,但並非必定能達到的。

不像以前的堆內存是按分代連續分佈的,G1 將堆劃分紅了必定數量(典型的如2048個)的小區域(Region),每一個小區域多是 Eden, Survior 或者 Old 區域。在邏輯上,Eden 和 Survivor 區域組成了新生代,Old 區域組成了老年代。這個設計容許 GC 每一個只收集其中的一些區域而不是整個堆。不過新生代區域每次都會參與。另外一個比較新奇的特色是 G1 能夠估計每一個區域存活對象的多少,存活數量多的區域會先被收集,這也是這個收集器名字的由來,garbage first 收集器。

G1 大體可分爲 Young GC 和 Mixed GC,Young GC 處理全部的 Young Region, Mixed GC 處理全部的 Young Region 和 部分的 Old Region,至於選擇哪些 Old Region,須要併發標記來蒐集必要的信息。分代式G1的正常工做流程就是在 young GC 與 mixed GC 之間視狀況切換,背後按期作併發標記蒐集資料。

Young GC —— 只回收年輕代的疏散階段 (Evacuation Pause: Fully Young)

在應用程序剛開始的時候,G1 沒有額外的信息去運行併發階段。所以在這個階段,G1 的運做模式和其餘的新生代收集器很像,須要 stop-the-world,使用複製算法,將存活對象的複製到 Survivor 區域中,或者空閒區域中(以後這個區域也是 Survivor 區)。該階段能夠理解爲 Young GC (Minor GC),所謂疏散,其實就是 copy 到其餘區域中。

併發標記 (Concurrent Marking)

G1 收集器的不少概念是創建在 CMS 上的,所以在流程上會有一些類似。儘管如此,它們之間也有不少不同的地方,好比 CMS 則是採用增量更新的方式,即額外 mark 修改的引用再作處理,而 G1 使用了 SATB (Snapshot-At-The-Beginning) 來維持併發 GC 的正確性 。Snapshot-At-The-Beginning,從字面上理解,就是在 GC 開始時產生一個全部存活對象的邏輯快照,在這個快照中存活的對象,加上以後在 GC 過程當中新分配的對象認爲是最終的存活對象,其它不可到達的對象就是死的了。關於 SATB 具體是如何工做的,能夠看R大寫過的 G1 講解

和 CMS 同樣,該階段具體的能夠分爲如下幾個階段,每一個階段的目的和 CMS 差很少,只是具體實現上會有區別:

  1. 初始標記(Initial Mark)
  2. 併發標記(concurrent marking)
  3. 最終標記(remark)
  4. 清理(cleanup)

其中 Initial Mark 在 Evacuation Pause 中捎帶完成了。注意這裏的 cleanup,並非真的在堆上清理了實際對象,而是統計每一個 region 存活數量的多少,並按預期的 GC 效率對它們進行排序,爲 mixed GC 作準備。但有例外,若是發現這個 region 都沒有存活對象,整個區域會被回收到可分配的 region 列表中。

Mixed GC —— 混合的疏散階段 (Evacuation Pause: Mixed)

這個階段並不必定緊跟在併發標記以後,須要知足必定的條件,好比,若是能夠同時釋放一大部分的老年代,不然該階段沒有必要。所以,在併發標記結束和混合疏散暫停之間可能很容易出現一些只回收年輕的疏散階段。

該階段選定全部新生代裏的 region,外加根據併發標記統計得出收集收益高的若干個老年代 region(在用戶指定的開銷目標範圍內儘量選擇收益高的old gen region)進行回收。

相關文章
相關標籤/搜索