1. 程式人生 > >【JVM進階之路】七:垃圾收集器盤點

【JVM進階之路】七:垃圾收集器盤點

在前面,我們已經瞭解了JVM的分代收集,知道JVM垃圾收集在新生代主要採用`標記-複製`演算法,在老年代主要採用`標記-清除`和`標記-整理`演算法。接下來,我們看一看JDK預設虛擬機器HotSpot的一些垃圾收集器的實現。 # 1、常見垃圾回收器 首先來看一下JDK 11之前全部可用的垃圾收集器。 ![HotSpot虛擬機器](https://gitee.com/sanfene/picgo/raw/master/20210328141650.png) 圖中列出了七種垃圾收集器,連線表示可以配合使用,所在區域表示它是屬於新生代收集器或是老年代收集器。 這裡還標出了垃圾收集器採用的收集演算法,G1收集器比較特殊,整體採用`標記-整理`演算法,區域性採用`標記-複製`演算法,後面再細講。 ## 1.1、Serial收集器 Serial收集器是最基礎、歷史最悠久的收集器。 如同它的名字(序列),它是一個單執行緒工作的收集器,使用一個處理器或一條收集執行緒去完成垃圾收集工作。並且進行垃圾收集時,必須暫停其他所有工作執行緒,直到垃圾收集結束——這就是所謂的“Stop The World”。 Serial/Serial Old收集器的執行過程如圖: ![Serial/Serial Old收集器執行示意圖](https://gitee.com/sanfene/picgo/raw/master/20210328230115.png) ## 1.2、ParNew收集器 ParNew收集器實質上是Serial收集器的多執行緒並行版本,使用多條執行緒進行垃圾收集。 ParNew收集器的工作過程如圖所示: ![ParNew/Serial Old收集器執行示意圖](https://gitee.com/sanfene/picgo/raw/master/20210328230326.png) 這裡值得一提的是Par是`Parallel(並行)`的縮寫,但需要注意的是,這個`並行(Parallel)`僅僅是描述同一時間多條GC執行緒協同工作,而不是GC執行緒和使用者執行緒同時執行。ParNew垃圾收集也是需要Stop The World的。 ## 1.3、Parallel Scavenge收集器 Parallel Scavenge收集器是一款新生代收集器,基於標記-複製演算法實現,也能夠並行收集。和ParNew有些類似,但Parallel Scavenge主要關注的是垃圾收集的吞吐量。 所謂吞吐量指的是執行使用者程式碼的時間與處理器總消耗時間的比值。這個比例越高,證明垃圾收集佔整個程式執行的比例越小。 ![吞吐量](https://gitee.com/sanfene/picgo/raw/master/20210329220706.png) Parallel Scavenge收集器提供了兩個引數用於精確控制吞吐量: - **-XX:MaxGCPauseMillis**,最大垃圾回收停頓時間。這個引數的原理是空間換時間,收集器會控制新生代的區域大小,從而儘可能保證回收少於這個最大停頓時間。簡單的說就是回收的區域越小,那麼耗費的時間也越小。 所以這個引數並不是設定得越小越好。設太小的話,新生代空間會太小,從而更頻繁的觸發GC。 - **-XX:GCTimeRatio**,垃圾收集時間與總時間佔比。這個是吞吐量的倒數,原理和MaxGCPauseMillis相同。 由於與吞吐量關係密切,Parallel Scavenge收集器也經常被稱作“吞吐量優先收集器”。 ## 1.4、Serial Old收集器 Serial Old是Serial收集器的老年代版本,它同樣是一個單執行緒收集器,使用標記-整理演算法。 Serial Old收集器的工作過程如圖: ![Serial/Serial Old收集器執行示意圖](https://gitee.com/sanfene/picgo/raw/master/20210328230115.png) ## 1.5、Parallel Old收集器 Parallel Old是Parallel Scavenge收集器的老年代版本,支援多執行緒併發收集,基於標記-整理演算法實現。 ![Parallel Scavenge/Parallel Old收集器執行示意圖](https://gitee.com/sanfene/picgo/raw/master/20210328230807.png) ## 1.6、CMS收集器 CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器,同樣是老年代的收集齊,採用`標記-清除`演算法。 CMS收集齊的垃圾收集分為四步: - `初始標記(CMS initial mark)`:單執行緒執行,需要Stop The World,標記GC Roots能直達的物件。 - `併發標記((CMS concurrent mark)`:無停頓,和使用者執行緒同時執行,從GC Roots直達物件開始遍歷整個物件圖。 - `重新標記(CMS remark)`:多執行緒執行,需要Stop The World,標記併發標記階段產生物件。 - `併發清除(CMS concurrent sweep)`:無停頓,和使用者執行緒同時執行,清理掉標記階段標記的死亡的物件。 > 涉及到了多次標記的過程,這裡插入一點`三色抽象`的知識。三色抽象用來描述物件在垃圾收集過程中的狀態。 > > 通常白色代表物件未被掃描到,灰色表示物件被掃描到但未被處理,黑色表示物件及其後代已被處理。在CMS的標記和清除過程中就用到了這種抽象,詳細的可以檢視參考【5】。 Concurrent Mark Sweep收集器執行示意圖如下: ![Concurrent Mark Sweep收集器執行示意圖](https://gitee.com/sanfene/picgo/raw/master/20210328232431.png) `優點`:CMS最主要的優點在名字上已經體現出來——併發收集、低停頓。 `缺點`:CMS同樣有三個明顯的缺點。 - Mark Sweep演算法會導致記憶體碎片比較多 - CMS的併發能力比較依賴於CPU資源,併發回收時垃圾收集執行緒可能會搶佔使用者執行緒的資源,導致使用者程式效能下降。 - 併發清除階段,使用者執行緒依然在執行,會產生所謂的理“浮動垃圾”(Floating Garbage),本次垃圾收集無法處理浮動垃圾,必須到下一次垃圾收集才能處理。如果浮動垃圾太多,會觸發新的垃圾回收,導致效能降低。 ## 1.7、Garbage First收集器 Garbage First(簡稱G1)收集器是垃圾收集器的一個顛覆性的產物,它開創了局部收集的設計思路和基於Region的記憶體佈局形式。 雖然G1也仍是遵循分代收集理論設計的,但其堆記憶體的佈局與其他收集器有非常明顯的差異。以前的收集器分代是劃分新生代、老年代、持久代等。 ![垃圾分代區域](https://gitee.com/sanfene/picgo/raw/master/20210329230703.png) G1把連續的Java堆劃分為多個大小相等的獨立區域(Region),每一個Region都可以根據需要,扮演新生代的Eden空間、Survivor空間,或者老年代空間。收集器能夠對扮演不同角色的Region採用不同的策略去處理。 ![G1 Heap Regions](https://gitee.com/sanfene/picgo/raw/master/20210329230612.png) 這樣就避免了收集整個堆,而是按照若干個Region集進行收集,同時維護一個優先順序列表,跟蹤各個Region回收的“價值,優先收集價值高的Region。 G1收集器的執行過程大致可劃分為以下四個步驟: - 初始標記(initial mark),標記了從GC Root開始直接關聯可達的物件。STW(Stop the World)執行。 - 併發標記(concurrent marking),和使用者執行緒併發執行,從GC Root開始對堆中物件進行可達性分析,遞迴掃描整個堆裡的物件圖,找出要回收的物件、 - 最終標記(Remark),STW,標記再併發標記過程中產生的垃圾。 - 篩選回收(Live Data Counting And Evacuation),制定回收計劃,選擇多個Region 構成回收集,把回收集中Region的存活物件複製到空的Region中,再清理掉整個舊 Region的全部空間。需要STW。 ![G1收集器執行示意圖](https://gitee.com/sanfene/picgo/raw/master/20210328232931.png) 相比CMS,G1的優點有很多,可以指定最大停頓時間、分Region的記憶體佈局、按收益動態確定回收集。 只從記憶體的角度來看,與CMS的“標記-清除”演算法不同,G1從整體來看是基於“標記-整理”演算法實現的收集器,但從區域性(兩個Region 之間)上看又是基於“標記-複製”演算法實現,無論如何,這兩種演算法都意味著G1運作期間不會產生記憶體空間碎片,垃圾收集完成之後能提供規整的可用記憶體。 # 2、前沿垃圾回收器 ## 2.1、ZGC收集器 在JDK 11當中,加入了實驗性質的ZGC。它的回收耗時平均不到2毫秒。它是一款低停頓高併發的收集器。 與CMS中的ParNew和G1類似,ZGC也採用標記-複製演算法,不過ZGC對該演算法做了重大改進:ZGC在標記、轉移和重定位階段幾乎都是併發的,這是ZGC實現停頓時間小於10ms目標的最關鍵原因。 ![ZGC垃圾回收週期](https://gitee.com/sanfene/picgo/raw/master/20210329233130.png) ZGC雖然在JDK 11還處於實驗階段,但由於演算法與思想是一個非常大的提升,未來前景相信還是很廣闊的。 # 3、垃圾收集器選擇 ## 3.1、收集器選擇權衡 垃圾收集器的選擇需要權衡的點還是比較多的——例如執行應用的基礎設施如何?使用JDK的發行商是什麼?等等…… 這裡簡單地列一下上面提到的一些收集器的適用場景: - **Serial** :如果應用程式有一個很小的記憶體空間(大約100 MB)亦或它在沒有停頓時間要求的單執行緒處理器上執行。 - **Parallel**:如果優先考慮應用程式的峰值效能,並且沒有時間要求要求,或者可以接受1秒或更長的停頓時間。 - **CMS/G1**:如果響應時間比吞吐量優先順序高,亦或垃圾收集暫停必須保持在大約1秒以內。 - **ZGC**:如果響應時間是高優先順序的,亦或堆空間比較大。 ## 3.1、設定垃圾收集器 設定垃圾收集器(組合)的引數如下: | **新生代** | **老年代** | **JVM 引數** | | --------------------- | ---------------- | -------------------------------------------- | | Incremental | Incremental | -Xincgc | | **Serial** | **Serial** | **-XX:+UseSerialGC** | | Parallel Scavenge | Serial | -XX:+UseParallelGC -XX:-UseParallelOldGC | | Parallel New | Serial | N/A | | Serial | Parallel Old | N/A | | **Parallel Scavenge** | **Parallel Old** | **-XX:+UseParallelGC -XX:+UseParallelOldGC** | | Parallel New | Parallel Old | N/A | | Serial | CMS | -XX:-UseParNewGC -XX:+UseConcMarkSweepGC | | Parallel Scavenge | CMS | N/A | | **Parallel New** | **CMS** | **-XX:+UseParNewGC -XX:+UseConcMarkSweepGC** | | **G1** | | **-XX:+UseG1GC** |
參考:
【1】:周志朋編著《深入理解Java虛擬機器:JVM高階特性與最佳實踐》 【2】:《垃圾回收演算法手冊 自動記憶體管理的藝術》 【3】:[Garbage Collection in Java – What is GC and How it Works in the JVM](https://www.freecodecamp.org/news/garbage-collection-in-java-what-is-gc-and-how-it-works-in-the-jvm/) 【4】:[Java Hotspot G1 GC的一些關鍵技術](https://tech.meituan.com/2016/09/23/g1.html) 【5】:[GC Algorithms: Implementations](https://plumbr.io/handbook/garbage-collection-algorithms-implementations#concurrent-mark-and-sweep) 【6】:[新一代垃圾回收器ZGC的探索與實踐](https://tech.meituan.com/2020/08/06/new-zgc-practice-in-meitu