天天看點

【JVM筆記】GC算法和GC收集器詳解一、Serial收集器二、ParNew收集器三、Parallel Scavenge收集器 四、Serial Old收集器五、Parallel Old收集器六、CMS收集器七、G1收集器八、怎麼選擇垃圾收集器?

目錄

一、Serial收集器

二、ParNew收集器

三、Parallel Scavenge收集器

四、Serial Old收集器

五、Parallel Old收集器

六、CMS收集器

為什麼除了Serial收集器外隻有ParNew能與CMS收集器配合?     

七、G1收集器

分區和卡片的關系

G1收集器的特點

G1收集器的執行步驟:

八、怎麼選擇垃圾收集器?

有關GC算法講解見:【JVM筆記】GC算法詳解

java虛拟機規範對垃圾收集器應該如何實作沒有任何規定,因為沒有所謂最好的垃圾收集器出現,更不會有萬金油垃圾收集器,隻能是根據具體的應用場景選擇合适的垃圾收集器。JVM虛拟機中有很多垃圾收集器,在絕大多數情況下JVM會自動選擇合适的收集器進行垃圾回收。

每個收集器都有自己适合的分代,收集器之間也大多是成對配合使用的,如下圖所示。

【JVM筆記】GC算法和GC收集器詳解一、Serial收集器二、ParNew收集器三、Parallel Scavenge收集器 四、Serial Old收集器五、Parallel Old收集器六、CMS收集器七、G1收集器八、怎麼選擇垃圾收集器?

一、Serial收集器

Serial收集器(串行收集器)是最基本、曆史最悠久的垃圾收集器了。大家看名字就知道這個收集器是一個單線程收集器了。它的 “單線程” 的意義不僅僅意味着它隻會使用一條垃圾收集線程去完成垃圾收集工作,更重要的是它在進行垃圾收集工作的時候必須暫停其他所有的工作線程( “Stop The World” ,簡稱STW),直到它收集結束。它在單核CPU時代被廣泛應用。

新生代采用複制算法,老年代(需要其他收集器配合)采用标記-整理算法。

【JVM筆記】GC算法和GC收集器詳解一、Serial收集器二、ParNew收集器三、Parallel Scavenge收集器 四、Serial Old收集器五、Parallel Old收集器六、CMS收集器七、G1收集器八、怎麼選擇垃圾收集器?

虛拟機的設計者們當然知道Stop The World帶來的不良使用者體驗,是以在後續的垃圾收集器設計中停頓時間在不斷縮短(仍然還有停頓,尋找最優秀的垃圾收集器的過程仍然在繼續)。

但是Serial收集器有沒有優于其他垃圾收集器的地方呢?當然有,它簡單而高效(與其他收集器的單線程相比)。Serial收集器由于沒有線程互動的開銷,自然可以獲得很高的單線程收集效率。Serial收集對于運作在Client模式下的虛拟機來說是個不錯的選擇。

二、ParNew收集器

ParNew收集器其實就是Serial收集器的多線程版本,除了使用多線程進行垃圾收集外,其餘行為(控制參數、收集算法、回收政策等等)和Serial收集器完全一樣。ParNew收集器在執行的時候會有多條垃圾收集線程并行工作,但也是會暫停其他所有程序(STW)。

新生代采用複制算法,老年代(需要其他收集器配合)采用标記-整理算法。

【JVM筆記】GC算法和GC收集器詳解一、Serial收集器二、ParNew收集器三、Parallel Scavenge收集器 四、Serial Old收集器五、Parallel Old收集器六、CMS收集器七、G1收集器八、怎麼選擇垃圾收集器?

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

三、Parallel Scavenge收集器

Parallel Scavenge 收集器類(并行收集器)似于ParNew 收集器。它是jdk1.8的預設是預設收集器。

Parallel Scavenge收集器關注點是吞吐量(高效率的利用CPU)。CMS等垃圾收集器的關注點更多的是使用者線程的停頓時間(提高使用者體驗)。所謂吞吐量就是CPU中用于運作使用者代碼的時間與CPU總消耗時間的比值。 Parallel Scavenge收集器提供了很多參數供使用者找到最合适的停頓時間或最大吞吐量,如果對于收集器運作不太了解的話,手工優化存在困難的話可以選擇把記憶體管理優化交給虛拟機去完成也是一個不錯的選擇。

新生代采用複制算法,老年代(需要其他收集器配合)采用标記-整理算法。

【JVM筆記】GC算法和GC收集器詳解一、Serial收集器二、ParNew收集器三、Parallel Scavenge收集器 四、Serial Old收集器五、Parallel Old收集器六、CMS收集器七、G1收集器八、怎麼選擇垃圾收集器?

Parallel Scavenge收集器執行的時候多條垃圾收集線程并行工作,在多核CPU下效率更高,應用線程仍然處于等待狀态(STW),但是因為他是多個GC線程并行執行垃圾回收,是以垃圾回收的比較快,應用線程等待的時間比Serial收集器少很多。 

四、Serial Old收集器

Serial收集器的老年代版本,它同樣是一個單線程收集器。它主要有兩大用途:一種用途是在JDK1.5以及以前的版本中與Parallel Scavenge收集器搭配使用,另一種用途是作為CMS收集器的後備方案。

五、Parallel Old收集器

Parallel Scavenge收集器的老年代版本。使用多線程和“标記-整理”算法。在注重吞吐量以及CPU資源的場合,都可以優先考慮 Parallel Scavenge收集器和Parallel Old收集器。

六、CMS收集器

并行和并發概念補充:

  • 并行(Parallel) :指多條垃圾收集線程并行工作,但此時使用者線程仍然處于等待狀态。并行指兩個或多個事件在同一時刻發生。并行一般就需要多CPU支援。
  • 并發(Concurrent):指使用者線程與垃圾收集線程同時執行(但不一定是并行,可能會交替執行)。并發是指兩個或多個事件在同一時間間隔内發生。

CMS(Concurrent Mark Sweep)收集器是一種以擷取最短回收停頓時間為目标的收集器。它而非常符合在注重使用者體驗的應用上使用。

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

從名字中的Mark Sweep這兩個詞可以看出,CMS收集器是一種 “标記-清除”算法實作的,它的運作過程相比于前面幾種垃圾收集器來說更加複雜一些。整個過程分為四個步驟:

  • 初始标記(CMS initial mark): 暫停所有的其他線程,并記錄下直接與root相連的對象,速度很快。多線程标記。
  • 并發标記(CMS concurrent mark): 同時開啟并發标記線程和使用者線程,用一個閉包結構去從GC Root開始對堆中對象進行可達性分析,找出存活的對象可達對象。但在這個階段結束,這個閉包結構并不能保證包含目前所有的可達對象。因為使用者線程可能會不斷的更新引用域,是以GC線程無法保證可達性分析的實時性。是以這個算法裡會跟蹤記錄這些發生引用更新的地方。
  • 重新标記(CMS remark): 重新标記階段就是為了修正并發标記期間因為使用者程式繼續運作而導緻标記産生變動的那一部分對象的标記記錄,這個階段的停頓時間一般會比初始标記階段的時間稍長,遠遠比并發标記階段時間短
  • 并發清除(CMS concurrent sweep): 開啟使用者線程,同時GC線程開始對為标記的區域做清掃。在這個期間就會産生浮動垃圾,就是在并發清理期間使用者線程執行期間還是有可能産生垃圾,這些垃圾在本次GC中是不能被回收的,這些垃圾就是浮動垃圾。浮動垃圾隻能等到下次GC被清除。
  • 并發重置:準備進行下一次GC

CMS收集器開啟後,年輕代使用STW式的并行收集(ParNew收集器),老年代回收采用CMS進行垃圾回收,對延遲的關注也主要展現在老年代CMS上。

【JVM筆記】GC算法和GC收集器詳解一、Serial收集器二、ParNew收集器三、Parallel Scavenge收集器 四、Serial Old收集器五、Parallel Old收集器六、CMS收集器七、G1收集器八、怎麼選擇垃圾收集器?

CMS主要優點:并發收集、低停頓。但是它有下面三個明顯的缺點:

  • 對CPU資源敏感;
  • 無法處理浮動垃圾;
  • 它使用的回收算法-“标記-清除”算法會導緻收集結束時會有大量空間碎片産生。

為什麼除了Serial收集器外隻有ParNew能與CMS收集器配合?     

CMS是HotSpot在JDK1.5推出的第一款真正意義上的并發(Concurrent)收集器,第一次實作了讓垃圾收集線程與使用者線程(基本上)同時工作;CMS作為老年代收集器,但卻無法與JDK1.4已經存在的新生代收集器Parallel Scavenge配合工作;因為Parallel Scavenge(以及G1)都沒有使用傳統的GC收集器代碼架構,而另外獨立實作;而其餘幾種收集器則共用了部分的傳統架構代碼,是以除了Serial收集器外,隻有ParNew能與CMS收集器配合。

七、G1收集器

之前介紹的幾組垃圾收集器組合,都有幾個共同點:

  • 年輕代、老年代是獨立且連續的記憶體塊;
  • 年輕代收集使用單eden、雙survivor進行複制算法;
  • 老年代收集必須掃描整個老年代區域;
  • 都是以盡可能少而快地執行GC為設計原則。

G1 (Garbage-First)是一款面向伺服器的垃圾收集器,主要針對配備多顆處理器及大容量記憶體的機器. 以極高機率滿足GC停頓時間要求的同時,還具備高吞吐量性能特征。同優秀的CMS垃圾回收器一樣,G1也是關注最小時延的垃圾回收器,也同樣适合大尺寸堆記憶體的垃圾收集,官方也推薦使用G1來代替選擇CMS。G1收集器在jdk1.9後成為了JVM的預設垃圾收集器。

【JVM筆記】GC算法和GC收集器詳解一、Serial收集器二、ParNew收集器三、Parallel Scavenge收集器 四、Serial Old收集器五、Parallel Old收集器六、CMS收集器七、G1收集器八、怎麼選擇垃圾收集器?

G1收集器放棄了之前的收集器中所使用的分代思想,引入分區(Region)的思路,弱化了分代的概念,合理利用垃圾收集各個周期的資源,解決了其他收集器甚至CMS的衆多缺陷。是以在G1中上面那個圖已經不适用了,而是要使用下面這個圖。

【JVM筆記】GC算法和GC收集器詳解一、Serial收集器二、ParNew收集器三、Parallel Scavenge收集器 四、Serial Old收集器五、Parallel Old收集器六、CMS收集器七、G1收集器八、怎麼選擇垃圾收集器?

分區和卡片的關系

  • 分區 Region

G1采用了分區(Region)的思路,将整個堆空間分成若幹個大小相等的記憶體區域,每次配置設定對象空間将逐段地使用記憶體。是以,在堆的使用上,G1并不要求對象的存儲一定是實體上連續的,隻要邏輯上連續即可;每個分區也不會确定地為某個代服務,可以按需在年輕代和老年代之間切換。

Humongous區是大對象存放的區域,在之前的收集器都有分比對擔保機制,在新生代的大對象如果存放不了就會被裝入老年代,但是G1收集器中大對象會被直接放到Humongous區。G1内部做了一個優化,一旦發現沒有引用指向大對象,則可直接在年輕代收集周期中被回收。

  • 卡片 Card

 在每個分區内部又被分成了若幹個大小為512 Byte卡片(Card),辨別堆記憶體最小可用粒度所有分區的卡片将會記錄在全局卡片表(Global Card Table)中,配置設定的對象會占用實體上連續的若幹個卡片,當查找對分區内對象的引用時便可通過記錄卡片來查找該引用對象。每次對記憶體的回收,都是對指定分區的卡片進行處理。

G1收集器被視為JDK1.7中HotSpot虛拟機的一個重要進化特征。它具備一下特點:

  • 并行與并發:G1能充分利用CPU、多核環境下的硬體優勢,使用多個CPU(CPU或者CPU核心)來縮短Stop-The-World停頓時間。部分其他收集器原本需要停頓Java線程執行的GC動作,G1收集器仍然可以通過并發的方式讓java程式繼續執行。G1的設計原則是"首先收集盡可能多的垃圾(Garbage First)",即先盡可能地标注可回收的對象,等到最後再根據使用者設定的等待停頓時間進行篩選回收。是以,G1并不會等記憶體耗盡(串行、并行)或者快耗盡(CMS)的時候開始垃圾收集,而是在内部采用了啟發式算法,它會日常進行垃圾收集,在老年代找出具有高收集收益的分區進行收集。同時G1可以根據使用者設定的暫停時間目标自動調整年輕代和總堆大小,暫停目标越短年輕代空間越小、總空間就越大;
  • 分代收集:雖然G1可以不需要其他收集器配合就能獨立管理整個GC堆(既能回收新生代,又能回收老年代),但是還是保留了分代的概念。G1采用記憶體分區(Region)的思路,将記憶體劃分為一個個相等大小的記憶體分區,回收時則以分區為機關進行回收,存活的對象複制到另一個空閑分區中。由于都是以相等大小的分區為機關進行操作,是以G1天然就是一種壓縮方案(局部壓縮);
  • 空間整合:與CMS的“标記–清理”算法不同,G1從整體來看是基于“标記-整理”算法實作的收集器;從局部上來看是基于“複制”算法實作的。G1的收集都是STW的,但年輕代和老年代的收集界限比較模糊,采用了混合(mixed)收集的方式。即每次收集既可能隻收集年輕代分區(年輕代收集Young GC),也可能在收集年輕代的同時,包含部分老年代分區(混合收集 Mixed GC),這樣即使堆記憶體很大時,也可以限制收集範圍,進而降低停頓。G1收集器隻有Young GC和 Mixed GC。
  • 可預測的停頓:這是G1相對于CMS的另一個大優勢,降低停頓時間是G1 和 CMS 共同的關注點,但G1 除了追求低停頓外,還能建立可預測的停頓時間模型,能讓使用者明确指定在一個長度為M毫秒的時間片段内。就是在篩選回收的過程中,對要進行回收的對象進行一次篩選,G1收集器會有一張表存有所有可回收的對象,這張表會按照對象引用類型來進行回收優先級的排序,如果使用者設定好停頓時間是10毫秒,假如收集器1毫秒隻能回收1000個垃圾對象,那麼G1收集器就會将表中回收優先級前10000個對象回收掉,剩下的可回收對象先把回收。通過這個機制就實作了可預測停頓,能讓使用者設定停頓時間。是以G1收集器很适合對使用者等待時長體驗要求很高的系統,可以自己設定合适的等待停頓時長。

還有一個問題:

G1把記憶體“化整為零”(将記憶體區域劃分成一個個的分區)的思路,以一個細節為例:把Java堆分為多個Region後,垃圾收集是否就真的能以Region為機關進行了?聽起來順理成章,再仔細想想就很容易發現問題所在:Region不可能是孤立的。一個對象配置設定在某個Region中,它并非隻能被本Region中的其他對象引用,而是可以與整個Java堆任意的對象發生引用關系。那在做可達性判定确定對象是否存活的時候,豈不是還得掃描整個Java堆才能保證準确性?這個問題其實并非在G1中才有,隻是在G1中更加突出而已。在以前的分代收集中,新生代的規模一般都比老年代要小許多,新生代的收集也比老年代要頻繁許多,那回收新生代中的對象時也面臨相同的問題,如果回收新生代時也不得不同時掃描老年代的話,那麼Minor GC的效率可能下降不少。

  • 這裡就要引入已記憶集合Remember Set (RSet)的概念:

在串行和并行收集器中,GC通過整堆掃描,來确定對象是否處于可達路徑中。然而G1為了避免STW式的整堆掃描,在每個分區記錄了一個已記憶集合(RSet),内部類似一個反向指針,記錄引用分區内對象的卡片索引。當要回收該分區時,通過掃描分區的RSet,來确定引用本分區内的對象是否存活,進而确定本分區内的對象存活情況。

事實上,并非所有的引用都需要記錄在RSet中,如果一個分區确定需要掃描,那麼無需RSet也可以無遺漏的得到引用關系。那麼引用源自本分區的對象,當然不用落入RSet中;同時,G1 GC每次都會對年輕代進行整體收集,是以引用源自年輕代的對象,也不需要在RSet中記錄。最後隻有老年代的分區可能會有RSet記錄,這些分區稱為擁有RSet分區(an RSet’s owning region)。

在G1收集器中,Region之間的對象引用以及其他收集器中的新生代與老年代之間的對象引用,虛拟機都是使用Remembered Set 來避免全堆掃描的。G1中每個Region都有一個與之對應的Remembered Set,虛拟機發現程式在對Reference類型的資料進行寫操作時,會産生一個Write Barrier暫時中斷寫操作,檢查Reference引用的對象是否處于不同的Region之中(在分代的例子中就是檢查是否老年代中的對象引用了新生代中的對象),如果是,便通過Card Table 把相關引用資訊記錄到被引用對象所屬的Region的Remembered Set之中。當進行記憶體回收時,在GC根節點的枚舉範圍中加入Remembered Set即可保證不對全堆掃描也不會有遺漏。

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

  • 初始标記(Initial Marking):與CMS一樣,該階段僅僅隻是标記一下GCRoots能直接關聯到的對象,并且修改TAMS(Next Top at Mark Start)的值,讓下一階段使用者程式并發運作時,能在正确可用的Region中建立新對象,初始标記需要将Mutator線程(Java應用線程)暫停掉,也就是需要一個STW的時間段,但耗時很短。初始标記是并發執行,直到所有的分區處理完就結束标記。
  • 并發标記(Concurrent Marking):并發标記線程在并發标記階段啟動,從GC Root開始對堆中對象進行可達性分析,找出存活的對象,這階段耗時較長,但可與使用者程式并發執行,不需要停頓時間。
  • 最終标記(Final Marking):重新标記(Remark)是最後一個标記階段。在該階段中,G1需要一個暫停的時間,為了修正在并發标記期間因使用者程式繼續運作而導緻标記産生變動的那一部分标記記錄,虛拟機将這段時間對象變化記錄線上程Remembered Set Logs裡面,最終标記階段需要把Remembered Set Logs的資料合并到Remembered Set中。這個階段也是并行執行的。
  • 篩選回收(Live Data Counting and Evacuation):首先對各個Region的回收價值和成本進行排序,根據使用者所期望的GC停頓時間來制定回收計劃,這個階段其實也可以做到與使用者程式一起并發執行,但是因為隻回收一部分Region,時間是使用者可控制的,而且停頓使用者線程将大幅提高收集效率,是以它也需要STW的時間段,并且是并行執行。

它的過程和CMS很像:

【JVM筆記】GC算法和GC收集器詳解一、Serial收集器二、ParNew收集器三、Parallel Scavenge收集器 四、Serial Old收集器五、Parallel Old收集器六、CMS收集器七、G1收集器八、怎麼選擇垃圾收集器?

G1收集器之是以能建立可預測的停頓時間模型,是因為它可以有計劃地避免在整個Java堆中進行全區域的垃圾收集。G1跟蹤各個Region裡面的垃圾堆積的價值大小(回收所獲得的空間大小以及回收所需時間的經驗值),G1收集器在背景維護了一個優先清單,每次根據允許的收集時間,優先選擇回收價值最大的Region(這也就是它的名字Garbage-First的由來)。這種使用Region劃分記憶體空間以及有優先級的區域回收方式,保證了G1收集器在有限時間内可以盡可能高的收集效率(把記憶體化整為零)。 

總結:

G1是一款非常優秀的垃圾收集器,不僅适合堆記憶體大的應用,同時也簡化了調優的工作。通過主要的參數初始和最大堆空間、以及最大容忍的GC暫停目标,就能得到不錯的性能;同時,我們也看到G1對記憶體空間的浪費較高,但通過“首先收集盡可能多的垃圾(Garbage First)”的設計原則,可以及時發現過期對象,進而讓記憶體占用處于合理的水準。

八、怎麼選擇垃圾收集器?

  1. 優先調整堆的大小讓伺服器自己來選擇
  2. 如果記憶體小于100m,使用串行收集器
  3. 如果是單核,并且沒有停頓時間的要求,串行或JVM自己選擇
  4. 如果允許停頓時間超過1秒,選擇并行或者JVM自己選
  5. 如果響應時間最重要,并且不能超過1秒,使用并發收集器

官方推薦G1,性能高。HotSpot在JVM上力推的垃圾收集器,并賦予G1取代CMS的使命