垃圾收集器

MXC肖某某發表於2020-05-14

垃圾收集器

GC垃圾回收演算法和垃圾收集器關係

天上飛的理念,要有落地的實現(垃圾收集器就是GC垃圾回收演算法的實現)

GC演算法是記憶體回收的方法論,垃圾收集器就是演算法的落地實現

GC演算法主要有以下幾種

  • 引用計數(幾乎不用,無法解決迴圈引用的問題)
  • 複製拷貝(用於新生代)
  • 標記清除(用於老年代)
  • 標記整理(用於老年代)

因為目前為止還沒有完美的收集器出現,更沒有萬能的收集器,只是針對具體應用最合適的收集器,進行分代收集(那個代用什麼收集器)

四種主要的垃圾收集器

  • Serial:序列回收 -XX:+UseSerialGC
  • Parallel:並行回收 -XX:+UseParallelGC
  • CMS:併發標記清除
  • G1
  • ZGC:(java 11 出現的)

Serial

序列垃圾回收器,它為單執行緒環境設計且值使用一個執行緒進行垃圾收集,會暫停所有的使用者執行緒,只有當垃圾回收完成時,才會重新喚醒主執行緒繼續執行。所以不適合伺服器環境

Parallel

並行垃圾收集器,多個垃圾收集執行緒並行工作,此時使用者執行緒也是阻塞的,適用於科學計算 / 大資料處理等弱互動場景,也就是說Serial 和 Parallel其實是類似的,不過是多了幾個執行緒進行垃圾收集,但是主執行緒都會被暫停,但是並行垃圾收集器處理時間,肯定比序列的垃圾收集器要更短

CMS

併發標記清除,使用者執行緒和垃圾收集執行緒同時執行(不一定是並行,可能是交替執行),不需要停頓使用者執行緒,網際網路公司都在使用,適用於響應時間有要求的場景。併發是可以有互動的,也就是說可以一邊進行收集,一邊執行應用程式。

G1

G1垃圾回收器將堆記憶體分割成不同區域,然後併發的進行垃圾回收

垃圾收集器總結

注意:並行垃圾回收在單核CPU下可能會更慢

檢視預設垃圾收集器

使用下面JVM命令,檢視配置的初始引數

-XX:+PrintCommandLineFlags

然後執行一個程式後,能夠看到它的一些初始配置資訊

-XX:InitialHeapSize=266376000 -XX:MaxHeapSize=4262016000 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC

移動到最後一句,就能看到 -XX:+UseParallelGC 說明使用的是並行垃圾回收

-XX:+UseParallelGC

預設垃圾收集器有哪些

Java中一共有7大垃圾收集器

  • UserSerialGC:序列垃圾收集器
  • UserParallelGC:並行垃圾收集器
  • UseConcMarkSweepGC:(CMS)併發標記清除
  • UseParNewGC:年輕代的並行垃圾回收器
  • UseParallelOldGC:老年代的並行垃圾回收器
  • UseG1GC:G1垃圾收集器
  • UserSerialOldGC:序列老年代垃圾收集器(已經被移除)

底層原始碼

各垃圾收集器的使用範圍

新生代使用的:

  • Serial Copying: UserSerialGC,序列垃圾回收器
  • Parallel Scavenge:UserParallelGC,並行垃圾收集器
  • ParNew:UserParNewGC,新生代並行垃圾收集器

老年區使用的:

  • Serial Old:UseSerialOldGC,老年代序列垃圾收集器
  • Parallel Compacting(Parallel Old):UseParallelOldGC,老年代並行垃圾收集器
  • CMS:UseConcMarkSwepp,並行標記清除垃圾收集器

各區都能使用的:

G1:UseG1GC,G1垃圾收集器

垃圾收集器就來具體實現這些GC演算法並實現記憶體回收,不同廠商,不同版本的虛擬機器實現差別很大,HotSpot中包含的收集器如下圖所示:

部分引數說明

  • DefNew:Default New Generation
  • Tenured:Old
  • ParNew:Parallel New Generation
  • PSYoungGen:Parallel Scavenge
  • ParOldGen:Parallel Old Generation

Java中的Server和Client模式

使用範圍:一般使用Server模式,Client模式基本不會使用

作業系統

  • 32位的Window作業系統,不論硬體如何都預設使用Client的JVM模式
  • 32位的其它作業系統,2G記憶體同時有2個cpu以上用Server模式,低於該配置還是Client模式
  • 64位只有Server模式

新生代下的垃圾收集器

序列GC(Serial)

序列GC(Serial)(Serial Copying)

是一個單執行緒單執行緒的收集器,在進行垃圾收集時候,必須暫停其他所有的工作執行緒直到它收集結束。

序列收集器是最古老,最穩定以及效率高的收集器,只使用一個執行緒去回收但其在垃圾收集過程中可能會產生較長的停頓(Stop-The-World 狀態)。 雖然在收集垃圾過程中需要暫停所有其它的工作執行緒,但是它簡單高效,對於限定單個CPU環境來說,沒有執行緒互動的開銷可以獲得最高的單執行緒垃圾收集效率,因此Serial垃圾收集器依然是Java虛擬機器執行在Client模式下預設的新生代垃圾收集器

對應JVM引數是:-XX:+UseSerialGC

開啟後會使用:Serial(Young區用) + Serial Old(Old區用) 的收集器組合

表示:新生代、老年代都會使用序列回收收集器,新生代使用複製演算法,老年代使用標記-整理演算法

-Xms10m -Xmx10m -XX:PrintGCDetails -XX:+PrintConmandLineFlags -XX:+UseSerialGC

並行GC(ParNew)

並行收集器,使用多執行緒進行垃圾回收,在垃圾收集,會Stop-the-World暫停其他所有的工作執行緒直到它收集結束

ParNew收集器其實就是Serial收集器新生代的並行多執行緒版本,最常見的應用場景時配合老年代的CMS GC工作,其餘的行為和Serial收集器完全一樣,ParNew垃圾收集器在垃圾收集過程中同樣也要暫停所有其他的工作執行緒。它是很多Java虛擬機器執行在Server模式下新生代的預設垃圾收集器。

常見對應JVM引數:-XX:+UseParNewGC 啟動ParNew收集器,隻影響新生代的收集,不影響老年代

開啟上述引數後,會使用:ParNew(Young區用) + Serial Old的收集器組合,新生代使用複製演算法,老年代採用標記-整理演算法

-Xms10m -Xmx10m -XX:PrintGCDetails -XX:+PrintConmandLineFlags -XX:+UseParNewGC

但是會出現警告,即 ParNew 和 Serial Old 這樣搭配,Java8已經不再被推薦

備註: -XX:ParallelGCThreads 限制執行緒數量,預設開啟和CPU數目相同的執行緒數

並行回收GC(Parallel)/ (Parallel Scavenge)

因為Serial 和 ParNew都不推薦使用了,因此現在新生代預設使用的是Parallel Scavenge,也就是新生代和老年代都是使用並行

Parallel Scavenge收集器類似ParNew也是一個新生代垃圾收集器,使用複製演算法,也是一個並行的多執行緒的垃圾收集器,俗稱吞吐量優先收集器。一句話:序列收集器在新生代和老年代的並行化

它關注的重點是:

可控制的吞吐量(Thoughput = 執行使用者程式碼時間 / (執行使用者程式碼時間 + 垃圾收集時間) ),也即比如程式執行100分鐘,垃圾收集時間1分鐘,吞吐量就是99%。高吞吐量意味著高效利用CPU時間,它多用於在後臺運算而不需要太多互動的任務。

自適應調節策略也是ParallelScavenge收集器與ParNew收集器的一個重要區別。(自適應調節策略:虛擬機器會根據當前系統的執行情況收集效能監控資訊,動態調整這些引數以提供最合適的停頓時間( -XX:MaxGCPauseMills))或最大的吞吐量。

常用JVM引數:-XX:+UseParallelGC 或 -XX:+UseParallelOldGC(可互相啟用)使用Parallel Scanvenge收集器

開啟該引數後:新生代使用複製演算法,老年代使用標記-整理演算法

-Xms10m -Xmx10m -XX:PrintGCDetails -XX:+PrintConmandLineFlags -XX:+UseParallelGC

老年代下的垃圾收集器

序列GC(Serial Old) / (Serial MSC)

Serial Old是Serial垃圾收集器老年代版本,它同樣是一個單執行緒的收集器,使用標記-整理演算法,這個收集器也主要是執行在Client預設的Java虛擬機器中預設的老年代垃圾收集器

在Server模式下,主要有兩個用途(瞭解,版本已經到8及以後)

  • 在JDK1.5之前版本中與新生代的Parallel Scavenge收集器搭配使用(Parallel Scavenge + Serial Old)
  • 作為老年代版中使用CMS收集器的後備垃圾收集方案。

配置方法:

-Xms10m -Xmx10m -XX:PrintGCDetails -XX:+PrintConmandLineFlags -XX:+UseSerialOldlGC

該垃圾收集器,目前已經不推薦使用了

並行GC(Parallel Old)/ (Parallel MSC)

Parallel Old收集器是Parallel Scavenge的老年代版本,使用多執行緒的標記-整理演算法,Parallel Old收集器在JDK1.6才開始提供。

在JDK1.6之前,新生代使用ParallelScavenge收集器只能搭配老年代的Serial Old收集器,只能保證新生代的吞吐量優先,無法保證整體的吞吐量。在JDK1.6以前(Parallel Scavenge + Serial Old)

Parallel Old正是為了在老年代同樣提供吞吐量優先的垃圾收集器,如果系統對吞吐量要求比較高,JDK1.8後可以考慮新生代Parallel Scavenge和老年代Parallel Old 收集器的搭配策略。在JDK1.8及後(Parallel Scavenge + Parallel Old)

JVM常用引數:

-XX +UseParallelOldGC:使用Parallel Old收集器,設定該引數後,新生代Parallel+老年代 Parallel Old

使用老年代並行收集器:

-Xms10m -Xmx10m -XX:PrintGCDetails -XX:+PrintConmandLineFlags -XX:+UseParallelOldlGC

併發標記清除GC(CMS)

CMS收集器(Concurrent Mark Sweep:併發標記清除)是一種以最短回收停頓時間為目標的收集器

適合應用在網際網路或者B/S系統的伺服器上,這類應用尤其重視伺服器的響應速度,希望系統停頓時間最短。

CMS非常適合堆記憶體大,CPU核數多的伺服器端應用,也是G1出現之前大型應用的首選收集器。

Concurrent Mark Sweep:併發標記清除,併發收集低停頓,併發指的是與使用者執行緒一起執行

開啟該收集器的JVM引數: -XX:+UseConcMarkSweepGC 開啟該引數後,會自動將 -XX:+UseParNewGC開啟,開啟該引數後,使用ParNew(young 區用)+ CMS(Old 區用) + Serial Old 的收集器組合,Serial Old將作為CMS出錯的後備收集器

-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+UseConcMarkSweepGC

四個步驟

  • 初始標記(CMS initial mark)
    • 只是標記一個GC Roots 能直接關聯的物件,速度很快,仍然需要暫停所有的工作執行緒
  • 併發標記(CMS concurrent mark)和使用者執行緒一起
    • 進行GC Roots跟蹤過程,和使用者執行緒一起工作,不需要暫停工作執行緒。主要標記過程,標記全部物件
  • 重新標記(CMS remark)
    • 為了修正在併發標記期間,因使用者程式繼續執行而導致標記產生變動的那一部分物件的標記記錄,仍然需要暫停所有的工作執行緒,由於併發標記時,使用者執行緒依然執行,因此在正式清理前,再做修正
  • 併發清除(CMS concurrent sweep)和使用者執行緒一起
    • 清除GC Roots不可達物件,和使用者執行緒一起工作,不需要暫停工作執行緒。基於標記結果,直接清理物件,由於耗時最長的併發標記和併發清除過程中,垃圾收集執行緒可以和使用者現在一起併發工作,所以總體上來看CMS收集器的記憶體回收和使用者執行緒是一起併發地執行。

優點:併發收集低停頓

缺點:併發執行,對CPU資源壓力大,採用的標記清除演算法會導致大量碎片

由於併發進行,CMS在收集與應用執行緒會同時增加對堆記憶體的佔用,也就是說,CMS必須在老年代堆記憶體用盡之前完成垃圾回收,否則CMS回收失敗時,將觸發擔保機制,序列老年代收集器將會以STW方式進行一次GC,從而造成較大的停頓時間

標記清除演算法無法整理空間碎片,老年代空間會隨著應用時長被逐步耗盡,最後將不得不通過擔保機制對堆記憶體進行壓縮,CMS也提供了引數 -XX:CMSFullGCSBeForeCompaction(預設0,即每次都進行記憶體整理)來指定多少次CMS收集之後,進行一次壓縮的Full GC

垃圾收集器如何選擇

組合的選擇

  • 單CPU或者小記憶體,單機程式
    • -XX:+UseSerialGC
  • 多CPU,需要最大的吞吐量,如後臺計算型應用
    • -XX:+UseParallelGC(這兩個相互啟用)
    • -XX:+UseParallelOldGC
  • 多CPU,追求低停頓時間,需要快速響應如網際網路應用
    • -XX:+UseConcMarkSweepGC
    • -XX:+ParNewGC
引數 新生代垃圾收集器 新生代演算法 老年代垃圾收集器 老年代演算法
-XX:+UseSerialGC SerialGC 複製 SerialOldGC 標記整理
-XX:+UseParNewGC ParNew 複製 SerialOldGC 標記整理
-XX:+UseParallelGC Parallel [Scavenge] 複製 Parallel Old 標記整理
-XX:+UseConcMarkSweepGC ParNew 複製 CMS + Serial Old的收集器組合,Serial Old作為CMS出錯的後備收集器 標記清除
-XX:+UseG1GC G1整體上採用標記整理演算法 區域性複製

G1垃圾收集器

開啟G1垃圾收集器

-XX:+UseG1GC

以前收集器的特點

  • 年輕代和老年代是各自獨立且連續的記憶體塊
  • 年輕代收集使用單eden + S0 + S1 進行復制演算法
  • 老年代收集必須掃描珍整個老年代區域
  • 都是以儘可能少而快速地執行GC為設計原則

G1是什麼

G1:Garbage-First 收集器,是一款面向服務端應用的收集器,應用在多處理器和大容量記憶體環境中,在實現高吞吐量的同時,儘可能滿足垃圾收集暫停時間的要求。另外,它還具有一下特徵:

  • 像CMS收集器一樣,能與應用程式併發執行
  • 整理空閒空間更快
  • 需要更多的時間來預測GC停頓時間
  • 不希望犧牲大量的吞吐量效能
  • 不需要更大的Java Heap

G1收集器設計目標是取代CMS收集器,它同CMS相比,在以下方面表現的更出色

  • G1是一個有整理記憶體過程的垃圾收集器,不會產生很多記憶體碎片。
  • G1的Stop The World(STW)更可控,G1在停頓時間上新增了預測機制,使用者可以指定期望停頓時間。

CMS垃圾收集器雖然減少了暫停應用程式的執行時間,但是它還存在著記憶體碎片問題。於是,為了去除記憶體碎片問題,同時又保留CMS垃圾收集器低暫停時間的優點,JAVA7釋出了一個新的垃圾收集器-G1垃圾收集器

G1是在2012年才在JDK1.7中可用,Oracle官方計劃在JDK9中將G1變成預設的垃圾收集器以替代CMS,它是一款面向服務端應用的收集器,主要應用在多CPU和大記憶體伺服器環境下,極大減少垃圾收集的停頓時間,全面提升伺服器的效能,逐步替換Java8以前的CMS收集器

主要改變時:Eden,Survivor和Tenured等記憶體區域不再是連續了,而是變成一個個大小一樣的region,每個region從1M到32M不等。一個region有可能屬於Eden,Survivor或者Tenured記憶體區域。

特點

  • G1能充分利用多CPU,多核環境硬體優勢,儘量縮短STW
  • G1整體上採用標記-整理演算法,區域性是通過複製演算法,不會產生記憶體碎片
  • 巨集觀上看G1之中不再區分年輕代和老年代。把記憶體劃分成多個獨立的子區域(Region),可以近似理解為一個圍棋的棋盤
  • G1收集器裡面將整個記憶體區域都混合在一起了,但其本身依然在小範圍內要進行年輕代和老年代的區分,保留了新生代和老年代,但他們不再是物理隔離的,而是通過一部分Region的集合且不需要Region是連續的,也就是說依然會採取不同的GC方式來處理不同的區域
  • G1雖然也是分代收集器,但整個記憶體分割槽不存在物理上的年輕代和老年代的區別,也不需要完全獨立的Survivor(to space)堆做複製準備,G1只有邏輯上的分代概念,或者說每個分割槽都可能隨G1的執行在不同代之間前後切換。

底層原理

Region區域化垃圾收集器,化整為零,打破了原來新生區和老年區的壁壘,避免了全記憶體掃描,只需要按照區域來進行掃描即可。

區域化記憶體劃片Region,整體遍為了一些列不連續的記憶體區域,避免了全記憶體區的GC操作。

核心思想是將整個堆記憶體區域分成大小相同的子區域(Region),在JVM啟動時會自動設定子區域大小

在堆的使用上,G1並不要求物件的儲存一定是物理上連續的,只要邏輯上連續即可,每個分割槽也不會固定地為某個代服務,可以按需在年輕代和老年代之間切換。啟動時可以通過引數-XX:G1HeapRegionSize=n 可指定分割槽大小(1MB~32MB,且必須是2的冪),預設將整堆劃分為2048個分割槽。

大小範圍在1MB~32MB,最多能設定2048個區域,也即能夠支援的最大記憶體為:32MB*2048 = 64G記憶體

Region區域化垃圾收集器

Region區域化垃圾收集器

G1將新生代、老年代的物理空間劃分取消了

同時對記憶體進行了區域劃分

G1演算法將堆劃分為若干個區域(Reign),它仍然屬於分代收集器,這些Region的一部分包含新生代,新生代的垃圾收集依然採用暫停所有應用執行緒的方式,將存活物件拷貝到老年代或者Survivor空間

這些Region的一部分包含老年代,G1收集器通過將物件從一個區域複製到另外一個區域,完成了清理工作。這就意味著,在正常的處理過程中,G1完成了堆的壓縮(至少是部分堆的壓縮),這樣也就不會有CMS記憶體碎片的問題存在了。

在G1中,還有一種特殊的區域,叫做Humongous(巨大的)區域,如果一個物件佔用了空間超過了分割槽容量50%以上,G1收集器就認為這是一個巨型物件,這些巨型物件預設直接分配在老年代,但是如果他是一個短期存在的巨型物件,就會對垃圾收集器造成負面影響,為了解決這個問題,G1劃分了一個Humongous區,它用來專門存放巨型物件。如果一個H區裝不下一個巨型物件,那麼G1會尋找連續的H區來儲存,為了能找到連續的H區,有時候不得不啟動Full GC。

回收步驟

針對Eden區進行收集,Eden區耗盡後會被觸發,主要是小區域收集 + 形成連續的記憶體塊,避免內碎片

  • Eden區的資料移動到Survivor區,加入出現Survivor區空間不夠,Eden區資料會晉升到Old區
  • Survivor區的資料移動到新的Survivor區,部分資料晉升到Old區
  • 最後Eden區收拾乾淨了,GC結束,使用者的應用程式繼續執行

回收完成後

小區域收集 + 形成連續的記憶體塊,最後在收集完成後,就會形成連續的記憶體空間,這樣就解決了記憶體碎片的問題

四步過程

  • 初始標記:只標記GC Roots能直接關聯到的物件
  • 併發標記:進行GC Roots Tracing(鏈路掃描)的過程
  • 最終標記:修正併發標記期間,因為程式執行導致標記發生變化的那一部分物件
  • 篩選回收:根據時間來進行價值最大化回收

引數配置

開發人員僅僅需要申明以下引數即可

三步歸納:-XX:+UseG1GC -Xmx32G -XX:MaxGCPauseMillis=100

-XX:MaxGCPauseMillis=n:最大GC停頓時間單位毫秒,這是個軟目標,JVM儘可能停頓小於這個時間

G1和CMS比較

  • G1不會產生內碎片
  • 是可以精準控制停頓。該收集器是把整個堆(新生代、老年代)劃分成多個固定大小的區域,每次根據允許停頓的時間去收集垃圾最多的區域。

SpringBoot結合JVMGC

啟動微服務時候,就可以帶上JVM和GC的引數

  • IDEA開發完微服務工程
  • maven進行clean package
  • 要求微服務啟動的時候,同時配置我們的JVM/GC的調優引數
    • 我們就可以根據具體的業務配置我們啟動的JVM引數

例如:

java -Xms1024m -Xmx1024 -XX:UseG1GC -jar   xxx.jar

相關文章