10. 系統分析垃圾收集器

盛開的太陽發表於2021-10-21

一、垃圾收集演算法

垃圾收集常用的演算法有三種。標記-清除演算法,標記-複製演算法,標記-整理演算法。下面一個一個來看:

1.1標記清除演算法

標記清除演算法分為“標記”和“清除”兩個階段:標記存活的物件, 統一回收所有未被標記的物件(一般選擇這種);也可以反過來,標 記出所有需要回收的物件,在標記完成後統一回收所有被標記的物件 。

1.1.1 標記清除演算法的原理

當堆中的有效記憶體空間(available memory)被耗盡的時候,就會停止整個程式(也被稱為stop the world),然後進行兩項工作,第一項則是標記,第二項則是清除。

  • 標記: Collector從引用根結點開始遍歷,標記所有被引用的物件。一般是在物件的Header中記錄為可達物件。

  • 清除: Collector對堆記憶體從頭到尾進行線性的遍歷,如果發現某個物件在其Header中沒有標記為可達物件,則將其回收。

10. 系統分析垃圾收集器

1.1.2 標記清除演算法存在的問題

標記清除演算法是最基礎的收集演算法,比較簡單,但是會帶來 兩個明顯的問題:

1. 效率問題

  • 如果需要標記的物件太多,效率不高
  • 如果記憶體空間太大,效率也不高

2. 空間問題

  • 標記清除後會產生大量不連續的碎片

1.2標記複製演算法

標記複製演算法包含兩個步驟:標記和複製。

1.2.1 標記複製演算法的原理

標記複製演算法的原理是,將指定的一塊記憶體分為大小相同的兩塊,每次使用其中的一塊。當這一塊的記憶體使用完後,就將還存活的物件複製到另一塊去,然後再把使用的空間清理掉。這樣就使每次的記憶體回收都是對記憶體區間的一半進行回收。不難想象,在下一次GC之後,左邊將會再次變成活動區間。如下圖:

10. 系統分析垃圾收集器

1.2.2 標記複製演算法存在的問題

標記複製演算法需要兩塊空間,對記憶體要求比較大,記憶體的利用率比較低。適用於短生存期的物件,持續複製長生存期的物件則導致效率降低

1.3 標記整理演算法

1.3.1 標記-整理演算法的原理

標記整理演算法的標記過程和標記-清除演算法一樣,因為標記清除演算法會導致很多留下來的記憶體空間碎片,隨著碎片的增多,嚴重影響記憶體讀寫的效能,所以在標記-清除之後,會對記憶體的碎片進行整理。讓所有存活的物件向一端移動,然後直接清理掉另一端的記憶體。由於壓縮空間需要一定的時間,會影響垃圾收集的時間。通常用在老年代,這也是老年代耗時多的原因之一。如下圖:

10. 系統分析垃圾收集器

1.3.2 標記整理演算法存在的問題

標記整理是標記清除的擴充套件版,在標記清除以後,對記憶體空間進行整理。這樣會更耗費時間。

二、分代收集理論

GC分代的基本假設:絕大部分物件的生命週期都非常短暫,存活時間短。

通常,我們將java堆分為新生代和老年代,“分代收集”(Generational Collection)就是根據堆劃分的特點採用最適當的收集演算法。在新生代中,每次垃圾收集時都發現有大批物件死去,只有少量存活,所以可以選擇複製演算法,只需要付出少量存活物件的複製成本就可以完成每次垃圾收集。而老年代中物件存活的概率是比較高的,而且沒有額外空間對它進行分配擔保,就使用“標記-清除”或“標記-整理”演算法來進行回收。“標記-清除”或“標記-整理”演算法會比複製演算法慢10倍以上。

三、垃圾收集器

垃圾收集器按照堆空間分類方法分為新生代垃圾收集器,老年代垃圾收集器。常見的新生代垃圾收集器有:Serial、ParNew、Parallel;常見的老年代垃圾收集器有:CMS、Serial Old、Parallel Old。還有既有新生代又有老年代的收集器,如:G1、ZGC等。不同型別的垃圾收集器採用的垃圾收集演算法是不同的。通常新生代使用的是標記-複製演算法;老年代使用的是標記清除和標記整理演算法。

常見的垃圾收集器如下圖:

10. 系統分析垃圾收集器

Serial、ParNew、Parallel Scavenge用於新生代;CMS、Serial Old、Paralled Old用於老年代。並且他們之間以相對固定的組合使用(具體組合關係如上圖)。G1是一個獨立的收集器不依賴其他6種收集器。ZGC是目前JDK 11的實驗收集器。下面來研究一下各種型別的垃圾收集器

3.1 Serial收集器

Serial(序列)收集器是最基本、歷史最悠久的垃圾收集器。Serial收集器是一個單執行緒收集器。它的 “單執行緒” 的意義不僅僅意味著它只會使用一條垃圾收集執行緒去完成垃圾收集工作,更重要的是它在進行垃圾收集工作的時候必須暫停其他所有的工作執行緒( "Stop The World" ),直到它收集結束。

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

10. 系統分析垃圾收集器

Serial收集器是新生代垃圾收集器,其對應的Serial Old是老年代垃圾收集器。Serial Old也是單執行緒收集器,它主要有兩大用途:

  • 一種用途是在JDK1.5以及以前的版本中與Parallel Scavenge收集器搭配使用,
  • 另一種用途是作為CMS收集器的後備方案。

Serial收集器引數配置

啟用Serial收集器, 啟用Serial Old收集器
-XX:+UseSerialGC 
-XX:+UseSerialOldGC

3.2 Parallel收集器

Parallel收集器其實就是Serial收集器的多執行緒版本,除了使用多執行緒進行垃圾收集外,其餘行為(控制引數、收集演算法、回收策略等等)和Serial收集器類似。預設的收集執行緒數跟cpu核數相同,當然也可以用引數(-XX:ParallelGCThreads)指定收集執行緒數,但是一般不推薦修改。

Parallel Scavenge收集器是一個新生代收集器,採用標記複製演算法,並行手機垃圾。該收集器關注點是吞吐量(高效率的利用CPU)。CMS等垃圾收集器的關注點更多的是使用者執行緒的停頓時間(提高使用者體驗)。

什麼是吞吐量呢?

就是CPU中用於執行使用者程式碼的時間與CPU總消耗時間的比值。 即:

10. 系統分析垃圾收集器

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

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

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

新生代採用複製演算法,老年代採用標記-整理演算法。

10. 系統分析垃圾收集器

Parallel垃圾收集器對應的老年代垃圾收集器是Parallel Old。Parallel Old採用的也是多執行緒收集垃圾。在注重吞吐量以及CPU資源的場合,都可以優先考慮 Parallel Scavenge收集器和Parallel Old收集器。

JDK8預設的新生代和老年代收集器

3.3 ParNew收集器

ParNew同樣用於新生代,跟Parallel收集器很類似,也是採用多執行緒的方式收集垃圾,Par是Parallel的縮寫。ParNew收集器工作的時候同樣需要STW(Stop The World)。ParNew主要和CMS收集器配合使用。另外Parallel收集器更多關注的是吞吐量。當對吞吐量以及CPU要求比較高的情況下,建議使用Parallel收集器。

因為是多執行緒執行,所以在多CPU下,ParNew效果通常會比Serial好。但如果是單CPU則會因為執行緒的切換,效能反而更差。

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

10. 系統分析垃圾收集器

引數設定

使用-XX:+UseConcMarkSweepGC選項後預設新生代收集器為ParNew收集器;
使用-XX:+UseParNewGC選項強制指定使用ParNew收集器;
使用-XX:ParallelGCThreads引數限制垃圾收集的執行緒數;

3.4 CMS收集器

1.什麼是CMS收集器

CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器。它在垃圾收集時使得使用者執行緒和 GC 執行緒併發執行,因此在垃圾收集過程中使用者也不會感到明顯的卡頓。是基於多執行緒的“標記-清除”演算法

CMS非常符合在注重使用者體驗的應用上使用,它是HotSpot虛擬機器第一款真正意義上的併發收集器,它第一次實現了讓垃圾收集執行緒與使用者執行緒(基本上)同時工作。

2.CMS收集器的工作原理

CMS整個過程比之前的收集器要複雜,整個過程分為四步:

10. 系統分析垃圾收集器

第一步:初始標記。(Stop The World) 只是標記一下 GC Roots 能直接關聯的物件,速度很快,仍然需要暫停所有的工作執行緒。

第二步:併發標記。(Stop The World) 進行 GC Roots 跟蹤的過程,和使用者執行緒一起工作,不需要暫停工作執行緒。

第三步:重新標記 。為了修正在併發標記期間,因使用者程式繼續執行而導致標記產生變動的那一部分物件的標記記錄,仍然需要暫停所有的工作執行緒。

第四步:併發清除 。這裡包含兩個步驟。併發清理和執行緒重置。

  • 併發清理: 開啟使用者執行緒,同時GC執行緒開始對未標記的區域做清掃。這個階段如果有新增物件會被標記為黑色不做任何處理(見下面三色標記演算法詳解)。 ,和使用者執行緒一起工作,不需要暫停工作執行緒。
  • 執行緒重置:重置本次GC過程中的標記資料。

由於耗時最長的併發標記併發清除過程中,垃圾收集執行緒可以和使用者一起併發工作,所以總體上來看CMS 收集器的記憶體回收和使用者執行緒是一起併發地執行。

是一款優秀的垃圾收集器,具有併發收集、低停頓的優點。但也有幾個非常明顯的缺點:

  • 對CPU資源敏感(會和服務搶資源);
  • 無法處理浮動垃圾(在併發標記和併發清理階段又產生垃圾,這種浮動垃圾只能等到下一次gc再清理了);
  • 它使用的回收演算法-“標記-清除”演算法會導致收集結束時會有大量空間碎片產生。我們可以通過引數設定讓jvm在執行完標記清除以後進行整理
XX:+UseCMSCompactAtFullCollection    //可以讓jvm在執行完標記清除後再做整理
  • 執行過程中的不確定性,會存在上一次垃圾回收還沒執行完,然後垃圾回收又被觸發的情況,特別是在併發標記和併發清理階段會出現,一邊回收,系統一邊執行,也許沒回收完就再次觸發full gc,也就是"concurrentmode failure",此時會進入stop the world,用serial old垃圾收集器來回收

3. cms相關的引數

1. -XX:+UseConcMarkSweepGC:啟用cms 
2. -XX:ConcGCThreads:併發的GC執行緒數 
3. -XX:+UseCMSCompactAtFullCollection:FullGC之後做壓縮整理(減少碎片) 
4. -XX:CMSFullGCsBeforeCompaction:多少次FullGC之後壓縮一次,預設是0,代表每次FullGC後都會壓縮一次
5. -XX:CMSInitiatingOccupancyFraction: 當老年代使用達到該比例時會觸發FullGC(預設是92,這是百分比) 
6. -XX:+UseCMSInitiatingOccupancyOnly:只使用設定的回收閾值(-XX:CMSInitiatingOccupancyFraction設 定的值),如果不指定,JVM僅在第一次使用設定值,後續則會自動調整 
7. -XX:+CMSScavengeBeforeRemark:在CMS GC前啟動一次minor gc,目的在於減少老年代對年輕代的引 用,降低CMS GC的標記階段時的開銷,一般CMS的GC耗時 80%都在標記階段 
8. -XX:+CMSParallellnitialMarkEnabled:表示在初始標記的時候多執行緒執行,縮短STW 
9. -XX:+CMSParallelRemarkEnabled:在重新標記的時候多執行緒執行,縮短STW;

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

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

相關文章