一文了解JVM全部垃圾回收器,從Serial到ZGC

公眾號__Zack說碼發表於2019-01-19

《物件搜尋演算法與回收演算法》介紹了垃圾回收的基礎演算法,相當於垃圾回收的方法論。接下來就詳細看看垃圾回收的具體實現。

上文提到過現代的商用虛擬機器的都是採用分代收集的,不同的區域用不同的收集器。常用的7種收集器,其適用的範圍如圖所示

一文了解JVM全部垃圾回收器,從Serial到ZGC
Serial、ParNew、Parallel Scavenge用於新生代;
CMS、Serial Old、Paralled Old用於老年代。 並且他們相互之間以相對固定的組合使用(具體組合關係如上圖)。G1是一個獨立的收集器不依賴其他6種收集器。ZGC是目前JDK 11的實驗收集器。

下面來看看各個收集器的特性

Serial收集器

Serial,是單執行緒執行垃圾回收的。當需要執行垃圾回收時,程式會暫停一切手上的工作,然後單執行緒執行垃圾回收。

因為新生代的特點是物件存活率低,所以收集演算法用的是複製演算法,把新生代存活物件複製到老年代,複製的內容不多,效能較好。

一文了解JVM全部垃圾回收器,從Serial到ZGC
單執行緒地好處就是減少上下文切換,減少系統資源的開銷。但這種方式的缺點也很明顯,在GC的過程中,會暫停程式的執行。若GC不是頻繁發生,這或許是一個不錯的選擇,否則將會影響程式的執行效能。 對於新生代來說,區域比較小,停頓時間短,所以比較使用。

ParNew收集器

ParNew同樣用於新生代,是Serial的多執行緒版本,並且在引數、演算法(同樣是複製演算法)上也完全和Serial相同。

Par是Parallel的縮寫,但它的並行僅僅指的是收集多執行緒並行,並不是收集和原程式可以並行進行。ParNew也是需要暫停程式一切的工作,然後多執行緒執行垃圾回收。

一文了解JVM全部垃圾回收器,從Serial到ZGC
因為是多執行緒執行,所以在多CPU下,ParNew效果通常會比Serial好。但如果是單CPU則會因為執行緒的切換,效能反而更差。

Parallel Scavenge收集器

新生代的收集器,同樣用的是複製演算法,也是並行多執行緒收集。與ParNew最大的不同,它關注的是垃圾回收的吞吐量。

這裡的吞吐量指的是 總時間與垃圾回收時間的比例。這個比例越高,證明垃圾回收佔整個程式執行的比例越小。

Parallel Scavenge收集器提供兩個引數控制垃圾回收的執行:

  • -XX:MaxGCPauseMillis,最大垃圾回收停頓時間。這個引數的原理是空間換時間,收集器會控制新生代的區域大小,從而儘可能保證回收少於這個最大停頓時間。簡單的說就是回收的區域越小,那麼耗費的時間也越小。
    所以這個引數並不是設定得越小越好。設太小的話,新生代空間會太小,從而更頻繁的觸發GC。
  • -XX:GCTimeRatio,垃圾回收時間與總時間佔比。這個是吞吐量的倒數,原理和MaxGCPauseMillis相同。

因為Parallel Scavenge收集器關注的是吞吐量,所以當設定好以上引數的時候,同時不想設定各個區域大小(新生代,老年代等)。可以開啟**-XX:UseAdaptiveSizePolicy**引數,讓JVM監控收集的效能,動態調整這些區域大小引數。

Serial Old收集器

老年代的收集器,與Serial一樣是單執行緒,不同的是演算法用的是標記-整理(Mark-Compact)。

一文了解JVM全部垃圾回收器,從Serial到ZGC
因為老年代裡面物件的存活率高,如果依舊是用複製演算法,需要複製的內容較多,效能較差。並且在極端情況下,當存活為100%時,沒有辦法用複製演算法。所以需要用Mark-Compact,以有效地避免這些問題。

Parallel Old收集器

老年代的收集器,是Parallel Scavenge老年代的版本。其中的演算法替換成Mark-Compact。

一文了解JVM全部垃圾回收器,從Serial到ZGC

CMS收集器

CMS,Concurrent Mark Sweep,同樣是老年代的收集器。它關注的是垃圾回收最短的停頓時間(低停頓),在老年代並不頻繁GC的場景下,是比較適用的。

命名中用的是concurrent,而不是parallel,說明這個收集器是有與工作執行併發的能力的。MS則說明演算法用的是Mark Sweep演算法。

來看看具體地工作原理。CMS整個過程比之前的收集器要複雜,整個過程分為四步:

  • 初始標記(initial mark),單執行緒執行,需要“Stop The World”,但僅僅把GC Roots的直接關聯可達的物件給標記一下,由於直接關聯物件比較小,所以這裡的速度非常快。
  • 併發標記(concurrent mark),對於初始標記過程所標記的初始標記物件,進行併發追蹤標記,此時其他執行緒仍可以繼續工作。此處時間較長,但不停頓。
  • 重新標記(remark),在併發標記的過程中,由於可能還會產生新的垃圾,所以此時需要重新標記新產生的垃圾。此處執行並行標記,與使用者執行緒不併發,所以依然是“Stop The World”,時間比初始時間要長一點。
  • 併發清除(concurrent sweep),併發清除之前所標記的垃圾。其他使用者執行緒仍可以工作,不需要停頓。

一文了解JVM全部垃圾回收器,從Serial到ZGC
由於最耗費時間的併發標記與併發清除階段都不需要暫停工作,所以整體的回收是低停頓的。

由於CMS以上特性,缺點也是比較明顯的,

  • Mark Sweep演算法會導致記憶體碎片比較多
  • CMS的併發能力依賴於CPU資源,所以在CPU數少和CPU資源緊張的情況下,效能較差
  • 併發清除階段,使用者執行緒依然在執行,所以依然會產生新的垃圾,此階段的垃圾並不會再本次GC中回收,而放到下次。所以GC不能等待記憶體耗盡的時候才進行GC,這樣的話會導致併發清除的時候,使用者執行緒可以了利用的空間不足。所以這裡會浪費一些記憶體空間給使用者執行緒預留。

有人會覺得既然Mark Sweep會造成記憶體碎片,那麼為什麼不把演算法換成Mark Compact呢?

答案其實很簡答,因為當併發清除的時候,用Compact整理記憶體的話,原來的使用者執行緒使用的記憶體還怎麼用呢?要保證使用者執行緒能繼續執行,前提的它執行的資源不受影響嘛。Mark Compact更適合“Stop the World”這種場景下使用。

G1收集器

G1,Garbage First,在JDK 1.7版本正式啟用,是當時最前沿的垃圾收集器。G1可以說是CMS的終極改進版,解決了CMS記憶體碎片、更多的記憶體空間登問題。雖然流程與CMS比較相似,但底層的原理已是完全不同。

高效益優先。G1會預測垃圾回收的停頓時間,原理是計算老年代物件的效益率,優先回收最大效益的物件。

堆記憶體結構的不同。以前的收集器分代是劃分新生代、老年代、持久代等。

一文了解JVM全部垃圾回收器,從Serial到ZGC

G1則是把記憶體分為多個大小相同的區域Region,每個Region擁有各自的分代屬性,但這些分代不需要連續。

一文了解JVM全部垃圾回收器,從Serial到ZGC

這樣的分割槽可以有效避免記憶體碎片化問題。

但是這樣同樣會引申一個新的問題,就是分代的記憶體不連續,導致在GC搜尋垃圾物件的時候需要全盤掃描找出引用記憶體所在。

為了解決這個問題,G1對於每個Region都維護一個Remembered Set,用於記錄物件引用的情況。當GC發生的時候根據Remembered Set的引用情況去搜尋。

兩種GC模式

  • Young GC,關注於所有年輕代的Region,通過控制收集年輕代的Region個數,從而控制GC的回收時間。
  • Mixed GC,關注於所有年輕代的Region,並且加上通過預測計算最大收益的若干個老年代Region。

整體的執行流程:

  • 初始標記(initial mark),標記了從GC Root開始直接關聯可達的物件。STW(Stop the World)執行。
  • 併發標記(concurrent marking),併發標記初始標記的物件,此時使用者執行緒依然可以執行。
  • 最終標記(Remark),STW,標記再併發標記過程中產生的垃圾。
  • 篩選回收(Live Data Counting And Evacuation),評估標記垃圾,根據GC模式回收垃圾。STW執行。

一文了解JVM全部垃圾回收器,從Serial到ZGC
在Region層面上,整體的演算法偏向於Mark-Compact。因為是Compact,會影響使用者執行緒執行,所以回收階段需要STW執行。

令人驚歎的ZGC

在JDK 11當中,加入了實驗性質的ZGC。它的回收耗時平均不到2毫秒。它是一款低停頓高併發的收集器。

ZGC幾乎在所有地方併發執行的,除了初始標記的是STW的。所以停頓時間幾乎就耗費在初始標記上,這部分的實際是非常少的。那麼其他階段是怎麼做到可以併發執行的呢?

ZGC主要新增了兩項技術,一個是著色指標Colored Pointer,另一個是讀屏障Load Barrier

著色指標Colored Pointer
ZGC利用指標的64位中的幾位表示Finalizable、Remapped、Marked1、Marked0(ZGC僅支援64位平臺),以標記該指向記憶體的儲存狀態。相當於在物件的指標上標註了物件的資訊。注意,這裡的指標相當於Java術語當中的引用。

在這個被指向的記憶體發生變化的時候(記憶體在Compact被移動時),顏色就會發生變化。

在G1的時候就說到過,Compact階段是需要STW,否則會影響使用者執行緒執行。那麼怎麼解決這個問題呢?

讀屏障Load Barrier 由於著色指標的存在,在程式執行時訪問物件的時候,可以輕易知道物件在記憶體的儲存狀態(通過指標訪問物件),若請求讀的記憶體在被著色了。那麼則會觸發讀屏障。讀屏障會更新指標再返回結果,此過程有一定的耗費,從而達到與使用者執行緒併發的效果。

把這兩項技術聯合下理解,引用R大(RednaxelaFX)的話

與標記物件的傳統演算法相比,ZGC在指標上做標記,在訪問指標時加入Load Barrier(讀屏障),比如當物件正被GC移動,指標上的顏色就會不對,這個屏障就會先把指標更新為有效地址再返回,也就是,永遠只有單個物件讀取時有概率被減速,而不存在為了保持應用與GC一致而粗暴整體的Stop The World。

ZGC雖然目前還在JDK 11還在實驗階段,但由於演算法與思想是一個非常大的提升,相信在未來不久會成為主流的GC收集器使用。


更多技術文章、精彩乾貨,請關注 部落格:zackku.com 微信公眾號:Zack說碼

一文了解JVM全部垃圾回收器,從Serial到ZGC

相關文章