JVM垃圾收集器(八)

童話述說我的結局發表於2022-02-09

一、垃圾收集器

有了前面JVM引數的瞭解下面來看下JVM的垃圾收集器;如果說收集演算法是記憶體回收的方法論,那麼垃圾收集器就是記憶體回收的具體實現。 

 JVM(HotSpot)有7種垃圾收集器,7種垃圾收集器作用於不同的分代,如果兩個收集器之間存在連續,就說明他們可以搭配使用。從JDK1.3到現在,從Serial收集器-》Parallel收集器-》CMS-》G1,使用者執行緒停頓時間不斷縮短,但仍然無法完全消除。

1.1、Serial收集器(序列收集器)

  Serial收集器是一個單執行緒的收集器。“單執行緒”的意義不僅僅是它只會使用一個CPU或一條收集器執行緒去完成垃圾收集工作,更重要的是它在垃圾收集的時候,必須暫停其他所有工作的執行緒,直到它收集結束。

  Serial收集器是HotSpot虛擬機器執行在Client模式下的預設新生代收集器。

  Serial收集器具有簡單而高效,由於沒有執行緒互動的開銷,可以獲得最高的單執行緒收集效率(在單個CPU環境中)。

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

1.1.1、優缺點

優點:簡單高效,擁有很高的單執行緒收集效率
缺點:收集過程需要暫停所有執行緒
演算法:複製演算法
適用範圍:新生代
應用:Client模式下的預設新生代收集器

1.2、ParNew收集器

 

 

 

ParNew收集器是Serial收集器的多執行緒版本,除了使用多條執行緒進行垃圾收集之外,其餘行為包括Serial收集器可用的所有控制引數、收集演算法、Stop The Word、物件分配規則、回收策略等都與Serial收集器一樣。

  ParNew收集器是許多執行在Server模式下的虛擬機器首選的新生代收集器,其中一個原因是,除了Serial收集器之外,目前只有ParNew收集器能與CMS收集器配合工作。 

  "-XX:+UseConcMarkSweepGC":指定使用CMS後,會預設使用ParNew作為新生代收集器。

       "-XX:+UseParNewGC":強制指定使用ParNew。   

       "-XX:ParallelGCThreads":指定垃圾收集的執行緒數量,ParNew預設開啟的收集執行緒與CPU的數量相同。

  並行(Parallel):指多條垃圾收集執行緒並行工作,但此時使用者執行緒仍然處於等待狀態。

  併發(Concurrent):指使用者執行緒與垃圾收集執行緒同時執行(但不一定是並行,可能是交替執行),使用者執行緒繼續工作,而垃圾收集程式執行在另一個CPU上。

 1.2.1、優缺點

優點:在多CPU時,比Serial效率高。
缺點:收集過程暫停所有應用程式執行緒,單CPU時比Serial效率差。
演算法:複製演算法
適用範圍:新生代
應用:執行在Server模式下的虛擬機器中首選的新生代收集器

1.3、Parallel Scavenge收集器

       Parallel Scavenge收集器是一個新生代收集器,使用複製演算法,且是並行的多執行緒收集器。

  Parallel Scavenge收集器關注點是達到一個可控制的吞吐量(吞吐量 = 執行使用者程式碼時間 / (執行使用者程式碼時間 + 垃圾收集時間)),而其他收集器關注點在儘可能的縮短垃圾收集時使用者執行緒的停頓時間。

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

  “ -XX:MaxGCPauseMillis” 引數允許的值是一個大於0的毫秒數,收集器將盡可能的保證記憶體垃圾回收花費的時間不超過設定的值(但是,並不是越小越好,GC停頓時間縮短是以犧牲吞吐量和新生代空間來換取的,如果設定的值太小,將會導致頻繁GC,這樣雖然GC停頓時間下來了,但是吞吐量也下來了)。

  “ -XX:GCTimeRatio”引數的值是一個大於0且小於100的整數,也就是垃圾收集時間佔總時間的比率,預設值是99,就是允許最大1%(即1/(1+99))的垃圾收集時間。

  “-XX:UseAdaptiveSizePolicy”引數是一個開發,如果這個引數開啟之後,虛擬機器會根據當前系統執行情況收集監控資訊,動態調整新生代的比例、老年大大小等細節引數,以提供最合適的停頓時間或最大的吞吐量,這種調節方式稱為GC自適應的調節策略。

1.4、Serial Old收集器

  Serial Old收集器是Seria收集器的老年代版本,他同樣是一個單執行緒收集器,使用" 標記-整理" 演算法,執行過程和Serial收集器一樣。

  Serial Old收集器主要用於Client模式下的虛擬機器使用。

  Server模式下的兩大用途:一、在JDK1.5及之前的版本與Parallel Scavenge收集器搭配使用;二、作為CMS收集器的後備方案,在併發收集發生Conturrent Mode Failure時使用。

1.5、Paraller Old收集器

       Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多執行緒和“標記-整理”演算法;也是更加關注系統的吞吐量。 在JDK1.6中才出現。

1.6、CMS(Conturrent Mark Sweep)收集器

  CMS收集器是一種以獲取最短回收停頓時間為目標的收集器。

  目前很大一部分的Java應用集中在網際網路或者B/S系統的服務端上。

  CMS收集器是基於“標記-清除”演算法實現,它的整個執行過程可以分為:初始登記(標記一下GC Roots能直接關聯到的物件,這個過程速度很快)、併發標記(進行GCRoots Tracing的過程)、重新標記(修正併發標記期間因使用者執行緒繼續運作而導致標記產生變動的那一部分物件的標記記錄,速度稍慢)、併發清除(清除死亡的物件)4個步驟;其中,初始標記和重新標記仍然需要“Stop The World”。

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

1.6.1、優缺點

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

  缺點:

    一:CMS收集器對CPU資源非常敏感。雖然在兩個併發階段不會導致使用者執行緒停頓,但是會因為佔用了一部分執行緒而導致應用程式變慢,總吞吐量下降。CMS預設啟動的回收執行緒數是(CPU數量+3)/4。

    二:CMS收集器無法處理浮動垃圾,可能出現“Conturrent Mode Failure”失敗而導致另一次Full GC產生。由於CMS併發清除階段使用者執行緒還在執行,伴隨著程式還在產生新的垃圾,這一部分垃圾出現在標記之後,CMS無法在當次收集中處理掉它們,只能留到下次再清理,這一部分垃圾稱為“浮動垃圾”。也正是由於在垃圾收集階段使用者執行緒還在執行,那麼也就需要預留有足夠的記憶體空間給使用者執行緒使用,因此CMS收集器不能像其他收集器那樣等待老年代填滿之後再進行收集,需要預留一部分空間給併發收集時使用者程式使用。可以通過“-XX:CMSInitiatingOccupancyFraction”引數設定老年代記憶體使用達到多少時啟動收集。

    三:由於CMS收集器是一個基於“標記-清除”演算法的收集器,那麼意味著收集結束會產生大量碎片,有時候往往還有很多記憶體未使用,可是沒有一塊連續的空間來分配一個物件,導致不得不提前觸發一次Full GC。CMS收集器提供了一個“-XX:UseCMSCompactAtFullCollection”引數(預設是開啟的)用於在CMS收集器頂不住要FullGC時開啟記憶體碎片整理(記憶體碎片整理意味著無法併發執行不得不停頓使用者執行緒)。引數“-XX:CMSFullGCsBeforeCompaction”來設定執行多少次不壓縮的Full GC後,跟著來一次帶壓縮的(預設值是0,意味著每次進入Full GC時都進行碎片整理)。

1.6.2、相關引數

//開啟CMS垃圾收集器 
-XX:+UseConcMarkSweepGC
//預設開啟,與-XX:CMSFullGCsBeforeCompaction配合使用
-XX:+UseCMSCompactAtFullCollection
//預設0 幾次Full GC後開始整理
-XX:CMSFullGCsBeforeCompaction=0
//輔助CMSInitiatingOccupancyFraction的引數,不然CMSInitiatingOccupancyFraction只會使 用一次就恢復自動調整,也就是開啟手動調整。
-XX:+UseCMSInitiatingOccupancyOnly
//取值0-100,按百分比回收
-XX:CMSInitiatingOccupancyFraction 預設-1
注意:CMS併發GC不是“full GC”。HotSpot VM裡對concurrent collection和full collection有明確的區分。所有帶有“FullCollection”字樣的VM引數都是跟真正的full GC相關,而跟CMS併發GC無關的。 

1.7、G1(Garbage-First)收集器

使用G1收集器時,Java堆的記憶體佈局與就與其他收集器有很大差別,它將整個Java堆劃分為多個大小相等的獨立區域(Region),雖然還保留有新生代和老年代的概念,但新生代和老年代不再是物理隔離的了,它們都是一部分Region(不需要連續)的集合。每個Region大小都是一樣的,可以是1M到32M之間的數值,根據總的記憶體大小而定,目標是數量不超過2048個,但是必須保證是2的n次冪;如果物件太大,一個Region放不下[超過Region大小的50%],那麼就會直接放到H中;設定Region大小:-XX:G1HeapRegionSize=M;所謂Garbage-Frist,其實就是優先回收垃圾最多的Region區域

每個RegionG1中扮演了不同的角色,比如Eden(新生區),比如Survivor(倖存區),或者Old(老年代)

除了傳統的老年代,新生代,G1還劃分出了Humongous區域,用來存放巨大物件(humongous object,H-obj)。

對於巨大物件,值得注意的有以下幾點:

  • H-obj的定義是大於等於Region一半的物件
  • H-obj直接分配到Old gen,防止頻繁拷貝。但是H-obj的回收卻不是在Mixed GC階段,而是concurrent marking階段中的clean up過程和full GC
    ❝ 這點一定注意,在調優過程中你會在GC日誌中經常發現這句
    [GC pause (G1 Humongous Allocation) (young) (initial-mark), 0.0029216 secs]
    疑惑點就在於為什麼Humongous Allocation卻是引發的yong gc
    原因便是在於為了通過yong gcinitial-mark開始進行concurrent marking,進而通過clean up回收大物件❞
    ❝ 如果想要檢視G1日誌的時候,為了方便快速達到GC的效果,你可能會直接分配一些大物件以便填滿整個堆從而引發GC,但是如果光是大物件,你可能會發現GC日誌中並沒有Mixed GC,而是頻繁的Yong GCConcurrent Marking,這便是原因❞
  • H-obj永遠不會被移動,雖然G1的回收演算法總體上看是基於標記-整理的,但是對於H-obj則永遠不會移動,要麼直接被回收,要麼一直存在。因此H-obj可能會導致較大的記憶體碎片進而引起頻繁的GC

1.7.1、G1收集器的特點

      G1是一款面向服務端應用的垃圾收集器,Sun(Oracle)賦予它的使命是(在比較長期的)未來可以替換掉JDK 5中釋出的CMS(Concurrent Mark Sweep)收集器,與其他GC收集器相比,G1具備如下特點:
  • 並行與併發:G1能充分利用多CPU、多核環境下的硬體優勢,使用多個CPU(CPU或者CPU核心)來縮短Stop-The-World停頓的時間,部分其他收集器原本需要停頓Java執行緒執行的GC動作,G1收集器仍然可以通過併發的方式讓Java程式繼續執行。
  • 分代收集:與其他收集器一樣,分代概念在G1中依然得以保留。雖然G1可以不需其他收集器配合就能獨立管理整個GC堆,但它能夠採用不同的方式去處理新建立的物件和已經存活了一段時間、熬過多次GC的舊物件以獲取更好的收集效果。
  • 空間整合:與CMS的“標記-清理”演算法不同,G1從整體看來是基於“標記-整理”演算法實現的收集器,從區域性(兩個Region之間)上看是基於“複製”演算法實現,無論如何,這兩種演算法都意味著G1運作期間不會產生記憶體空間碎片,收集後能提供規整的可用記憶體。這種特性有利於程式長時間執行,分配大物件時不會因為無法找到連續記憶體空間而提前觸發下一次GC。
  • 可預測的停頓:這是G1相對於CMS的另外一大優勢,降低停頓時間是G1和CMS共同的關注點,但G1除了追求低停頓外,還能建立可預測的停頓時間模型,能讓使用者明確指定在一個長度為M毫秒的時間片段內,消耗在垃圾收集上的時間不得超過N毫秒,這幾乎已經是實時Java(RTSJ)的垃圾收集器特徵了。

1.7.2、實現思路  

       在G1之前的其他收集器進行收集的範圍都是整個新生代或者老年代,而G1不再是這樣。使用G1收集器時,Java堆的記憶體佈局與就與其他收集器有很大差別,它將整個Java堆劃分為多個大小相等的獨立區域(Region),雖然還保留有新生代和老年代的概念,但新生代和老年代不再是物理隔離的了,它們都是一部分Region(不需要連續)的集合。
  G1收集器之所以能建立可預測的停頓時間模型,是因為它可以有計劃地避免在整個Java堆中進行全區域的垃圾收集。G1跟蹤各個Region裡面的垃圾堆積的價值大小(回收所獲得的空間大小以及回收所需時間的經驗值),在後臺維護一個優先列表,每次根據允許的收集時間,優先回價值最大的Region(這也就是Garbage-First名稱的來由)。這種使用Region劃分記憶體空間以及有優先順序的區域回收方式,保證了G1收集器在有限的時間內獲可以獲取儘可能高的收集效率。
  G1把記憶體“化整為零”的思路,理解起來似乎很容易理解,但其中的實現細節卻遠遠沒有現象中簡單,否則也不會從04年Sun實驗室發表第一篇G1的論文拖至今將近8年時間都還沒有開發出G1的商用版。筆者舉個一個細節為例:把Java堆分為多個Region後,垃圾收集是否就真的能以Region為單位進行了?聽起來順理成章,再仔細想想就很容易發現問題所在:Region不可能是孤立的。一個物件分配在某個Region中,它並非只能被本Region中的其他物件引用,而是可以與整個Java堆任意的物件發生引用關係。那在做可達性判定確定物件是否存活的時候,豈不是還得掃描整個Java堆才能保障準確性?這個問題其實並非在G1中才有,只是在G1中更加突出了而已。在以前的分代收集中,新生代的規模一般都比老年代要小許多,新生代的收集也比老年代要頻繁許多,那回收新生代中的物件也面臨過相同的問題,如果回收新生代時也不得不同時掃描老年代的話,Minor GC的效率可能下降不少。 
  在G1收集器中Region之間的物件引用以及其他收集器中的新生代與老年代之間的物件引用,虛擬機器都是使用Remembered Set來避免全堆掃描的。G1中每個Region都有一個與之對應的Remembered Set,虛擬機器發現程式在對Reference型別的資料進行寫操作時,會產生一個Write Barrier暫時中斷寫操作,檢查Reference引用的物件是否處於不同的Region之中(在分代的例子中就是檢查引是否老年代中的物件引用了新生代中的物件),如果是,便通過CardTable把相關引用資訊記錄到被引用物件所屬的Region的Remembered Set之中。當進行記憶體回收時,GC根節點的列舉範圍中加入Remembered Set即可保證不對全堆掃描也不會有遺漏。

1.7.3、運作過程

如果不計算維護Remembered Set的操作,G1收集器的運作大致可劃分為以下幾個步驟: 

  • 初始標記(Initial Marking)                                            標記以下GC Roots能夠關聯的物件,並且修改TAMS的值,需要暫停使用者執行緒 
  • 併發標記(Concurrent Marking)                                   從GC Roots進行可達性分析,找出存活的物件,與使用者執行緒併發執行
  • 最終標記(Final Marking)                                             修正在併發標記階段因為使用者程式的併發執行導致變動的資料,需暫停使用者執行緒 
  • 篩選回收(Live Data Counting and Evacuation)           對各個Region的回收價值和成本進行排序,根據使用者所期望的GC停頓時間制定回收計劃

  對CMS收集器運作過程熟悉的讀者,一定已經發現G1的前幾個步驟的運作過程和CMS有很多相似之處。初始標記階段僅僅只是標記一下GC Roots能直接關聯到的物件,並且修改TAMS(Next Top at Mark Start)的值,讓下一階段使用者程式併發執行時,能在正確可用的Region中建立新物件,這階段需要停頓執行緒,但耗時很短。併發標記階段是從GC Root開始對堆中物件進行可達性分析,找出存活的物件,這階段耗時較長,但可與使用者程式併發執行。而最終標記階段則是為了修正併發標記期間,因使用者程式繼續運作而導致標記產生變動的那一部分標記記錄,虛擬機器將這段時間物件變化記錄線上程Remembered Set Logs裡面,最終標記階段需要把Remembered Set Logs的資料合併到Remembered Set中,這階段需要停頓執行緒,但是可並行執行。最後篩選回收階段首先對各個Region的回收價值和成本進行排序,根據使用者所期望的GC停頓時間來制定回收計劃,從Sun透露出來的資訊來看,這個階段其實也可以做到與使用者程式一起併發執行,但是因為只回收一部分Region,時間是使用者可控制的,而且停頓使用者執行緒將大幅提高收集效率。通過圖1可以比較清楚地看到G1收集器的運作步驟中併發和需要停頓的階段。

JDK11新引入的ZGC收集器,不管是物理上還是邏輯上,ZGC中已經不存在新老年代的概念了會分為一個個page,當進行GC操作時會對page進行壓縮,因此沒有碎片問題,只能在64位的linux上使用,目前用得還比較少,有興趣的話可以去官網自己學習看下。

相關文章