1. 程式人生 > >JVM之GC演算法的實現(垃圾回收器)

JVM之GC演算法的實現(垃圾回收器)

上一節:《JVM之GC演算法》 知道GC演算法的理論基礎,我們來看看具體的實現。只有落地的理論,才是真理。

一、JVM垃圾回收器的結構

JVM虛擬機器規範對垃圾收集器應該如何實現沒有規定,因為沒有最好的垃圾收集器,只有最適合的場景。

 

 圖中展示了7種作用於不同分代的收集器,如果兩個收集器之間存在連線,則說明它們可以搭配使用。虛擬機器所處的區域則表示它是屬於新生代還是老年代收集器。

7種:serial收集器、parnew收集器、parallel scavenge收集器、serial  old 收集器、parallel old收集器、cms收集器、g1收集器(整堆收集器)、

序列收集:單垃圾收集執行緒,進行收集工作,使用者程序需要等待

並行收集:工作原理與序列一樣,只是在收集垃圾時是多條執行緒同時進行,收集的效率在一般情況下自然高於單執行緒。

併發收集:指使用者執行緒與垃圾收集執行緒同時工作(併發:同一時間間隔)。使用者程式在繼續執行,而垃圾收集程式執行在另一個CPU上。

吞吐量:吞吐量就是CPU中用於執行使用者程式碼的時間與CPU總消耗時間的比值(吞吐量 = 執行使用者程式碼時間 / (執行使用者程式碼時間 + 垃圾收集時間))

1、Serial收集器
Serial(序列)收集器:最基本,最古老的收集器,只有一個執行緒進行垃圾收集器的工作,並且在進行垃圾收集工作時需要暫停其他工作執行緒(stop the word),直到他工作結束;
Serial收集器簡單高效,工作時沒有執行緒互動的開銷,所以可以獲得很高的單執行緒收集效率,對於執行在Client模式下的虛擬機器來說很適合。

"-XX:+UseSerialGC":新增該引數來顯式的使用Serial垃圾收集器。

2、Serial Old收集器
  Serial Old收集器是Seria收集器的老年代版本,他同樣是一個單執行緒收集器,使用" 標記-整理" 演算法。
  Serial Old收集器主要用於Client模式下的虛擬機器使用。
  Server模式下的兩大用途:

    • 在JDK1.5及之前的版本與Parallel Scavenge收集器搭配使用;
    • 作為CMS收集器的後備方案,在併發收集發生Conturrent Mode Failure時使用。

3、ParNew 收集器
ParNew(並行)收集器就是Serial收集器的多執行緒版本,除了在收集垃圾時是啟用多執行緒並行執行,其他行為(控制引數、收集演算法、回收策略/Stop The Word、物件分配規則)完全一樣

 

 應用場景:ParNew收集器是許多執行在Server模式下的虛擬機器中首選的新生代收集器,因為它是除了Serial收集器外,唯一一個能與CMS收集器配合工作的。

"-XX:+UseConcMarkSweepGC":指定使用CMS後,會預設使用ParNew作為新生代收集器。
"-XX:+UseParNewGC":強制指定使用ParNew。
"-XX:ParallelGCThreads":指定垃圾收集的執行緒數量,ParNew預設開啟的收集執行緒與CPU的數量相同。

4、Parallel Scavenge收集器
Parallel Scavenge收集器 類似於 ParNew 收集器, Parallel Scavenge收集器 更加關注吞吐量(高效的CPU利用率)。CMS等垃圾收集器關注更多的是使用者執行緒的停頓時間(提搞使用者體驗);Parallel Scavenge 收集器提供很多引數供我們找到最合適的停頓時間或者最大吞吐量。JDK1.8 預設的方式;

Parallel Scavenge收集器提供了兩個引數來用於精確控制吞吐量,一是控制最大垃圾收集停頓時間的 -XX:MaxGCPauseMillis引數,二是控制吞吐量大小的 -XX:GCTimeRatio引數;

  • “ -XX:MaxGCPauseMillis” 引數允許的值是一個大於0的毫秒數,收集器將盡可能的保證記憶體垃圾回收花費的時間不超過設定的值(但是,並不是越小越好,GC停頓時間縮短是以犧牲吞吐量和新生代空間來換取的,如果設定的值太小,將會導致頻繁GC,這樣雖然GC停頓時間下來了,但是吞吐量也下來了)。
  • “ -XX:GCTimeRatio”引數的值是一個大於0且小於100的整數,也就是垃圾收集時間佔總時間的比率,預設值是99,就是允許最大1%(即1/(1+99))的垃圾收集時間。
  • “-XX:UseAdaptiveSizePolicy”引數是一個開發,如果這個引數開啟之後,虛擬機器會根據當前系統執行情況收集監控資訊,動態調整新生代的比例、老年大大小等細節引數,以提供最合適的停頓時間或最大的吞吐量,這種調節方式稱為GC自適應的調節策略。

應用場景:注重高吞吐量以及CPU資源敏感的場合,都可以優先考慮Parallel Scavenge+Parallel Old 收集器。

5、Paraller Old收集器
  Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多執行緒和“標記-整理”演算法。
  在JDK1.6中才出現。

6、CMS(Conturrent Mark Sweep)收集器

CMS收集器是一種以獲取最短回收停頓時間為目標的收集器。CMS收集器是基於“標記-清除”演算法實現,它的整個執行過程可以分為:

  • 初始標記:標記一下GC Roots能直接關聯到的物件,這個過程速度很快,但是會暫停其他使用者執行緒(Stop the word)
  • 併發標記:進行GCRoots Tracing的過程,同時開啟GC和使用者執行緒,用一個閉包的結構去記錄可達物件,但是在這個階段結束,該閉包不能保證其包含當前所有的可達物件。因為使用者程序可能會不斷的更新引用域,所以GC執行緒無法保證可達性分析的實時性。所以這個演算法會跟蹤記錄這些發生引用更新的地方。
  • 重新標記:修正併發標記期間因使用者執行緒繼續運作而導致標記產生變動的那一部分物件的標記記錄,該階段會GC停頓,停頓時間比初始標記時間稍長,單遠比並發標記時間短。
  • 併發清除:開啟使用者執行緒,同事GC執行緒清除死亡的物件

CMS收集器執行的整個過程中,最耗費時間的是併發標記和併發清除,GC收集器執行緒和使用者執行緒是一起工作的,所以總體來說,CMS收集器的記憶體回收過程是與使用者執行緒一起併發執行的。

優點:併發收集、低停頓。
缺點:

  • 1、CMS收集器對CPU資源非常敏感。雖然在兩個併發階段不會導致使用者執行緒停頓,但是會因為佔用了一部分執行緒而導致應用程式變慢,總吞吐量下降。CMS預設啟動的回收執行緒數是(CPU數量+3)/4。
  • 2、:CMS收集器無法處理浮動垃圾,可能出現“Conturrent Mode Failure”失敗而導致另一次Full GC產生。由於CMS併發清除階段使用者執行緒還在執行,伴隨著程式還在產生新的垃圾,這一部分垃圾出現在標記之後,CMS無法在當次收集中處理掉它們,只能留到下次再清理,這一部分垃圾稱為“浮動垃圾”。也正是由於在垃圾收集階段使用者執行緒還在執行,那麼也就需要預留有足夠的記憶體空間給使用者執行緒使用,因此CMS收集器不能像其他收集器那樣等待老年代填滿之後再進行收集,需要預留一部分空間給併發收集時使用者程式使用。可以通過“-XX:CMSInitiatingOccupancyFraction”引數設定老年代記憶體使用達到多少時啟動收集。
  • 3、:由於CMS收集器是一個基於“標記-清除”演算法的收集器,那麼意味著收集結束會產生大量碎片,有時候往往還有很多記憶體未使用,但是沒有一塊連續的空間來分配這個大物件,導致不得不提前觸發一次Full GC。CMS收集器提供了一個“-XX:UseCMSCompactAtFullCollection”引數(預設是開啟的)用於在CMS收集器頂不住要FullGC時開啟記憶體碎片整理(記憶體碎片整理意味著無法併發執行不得不停頓使用者執行緒)。引數“-XX:CMSFullGCsBeforeCompaction”來設定執行多少次不壓縮的Full GC後,跟著來一次帶壓縮的(預設值是0,意味著每次進入Full GC時都進行碎片整理)。

7、G1(Garbage-First)收集器

G1的記憶體模型

G1收集器沒有新生代和老年代的概念,而是將Java堆劃分為一塊塊獨立的大小相等的Region。當要進行垃圾收集時,首先估計每個Region中的垃圾數量,每次都從垃圾回收價值最大的Region開始回收,因此可以獲得最大的回收效率

Humongous是特殊的Old型別,專門放置大型物件.這樣的劃分方式意味著不需要一個連續的記憶體空間管理物件.G1將空間分為多個區域,優先回收垃圾最多的區域.
G1採用的是Mark-Copy ,有非常好的空間整合能力,不會產生大量的空間碎片
G1的一大優勢在於可預測的停頓時間,能夠儘可能快地在指定時間內完成垃圾回收任務,在JDK11中,已經將G1設為預設垃圾回收器,通過jstat命令可以檢視垃圾回收情況,在YGC時S0/S1並不會交換.

一個物件和它內部所引用的物件可能不在同一個Region中,那麼當垃圾回收時,是否需要掃描整個堆記憶體才能完整地進行一次可達性分析?
當然不是,每個Region都有一個Remembered Set,用於記錄本區域中所有物件引用的物件所在的區域,從而在進行可達性分析時,只要在GC Roots中再加上Remembered Set即可防止對所有堆記憶體的遍歷.

回收步驟

初始標記:標記與GC Roots直接關聯的物件,停止所有使用者執行緒,只啟動一條初始標記執行緒,這個過程很快.
併發標記:進行全面的可達性分析,開啟一條併發標記執行緒與使用者執行緒並行執行.這個過程比較長.
最終標記:標記出併發標記過程中使用者執行緒新產生的垃圾.停止所有使用者執行緒,並使用多條最終標記執行緒並行執行.
篩選回收:回收廢棄的物件.此時也需要停止一切使用者執行緒,並使用多條篩選回收執行緒並行執行.

G1為什麼能建立可預測的停頓時間模型?

因為它有計劃的避免在整個Java堆中進行全區域的垃圾收集。G1跟蹤各個Region裡面的垃圾堆積的大小,在後臺維護一個優先列表,每次根據允許的收集時間,優先回收價值最大的Region。這樣就保證了在有限的時間內可以獲取儘可能高的收集效率。

G1與其他收集器的區別?

其他收集器的工作範圍是整個新生代或者老年代、G1收集器的工作範圍是整個Java堆。在使用G1收集器時,它將整個Java堆劃分為多個大小相等的獨立區域(Region)。雖然也保留了新生代、老年代的概念,但新生代和老年代不再是相互隔離的,他們都是一部分Region(不需要連續)的集合。

二、如何選擇垃圾收集器

1、單CPU或者小記憶體,單機程式 — -XX:+UseSerialGC
2、多CPU,需要大吞吐量,如後臺計算型應用,允許工作執行緒停頓超過1秒  -XX:+UseParallelGC + -XX:+UseParallelOldGC
3、多CPU,追求低停頓時間,快速響應如網際網路應用  -XX:+UseParNewGC + -XX:+UseConcMarkSweepGC

4、JVM自己選擇

5、官方推薦G1,高性