一文搞懂——面試官常問的垃圾回收器


前言

本篇我們來詳細的看看JVM中常見的垃圾回收器有哪些以及每個垃圾回收器的特點,這也是面試的時候經常被問的內容

JVM堆內存概覽

在聊垃圾回收器之前,我們先來看看JVM堆內存的區域劃分是怎麼樣的,看下圖

  • 因爲虛擬機使用的垃圾回收算法是分代收集算法,所以堆內存被分爲了新生代和老年代
  • 新生代使用的垃圾回收算法是複製算法,所以新生代又被分爲了 Eden 和Survivor;空間大小比例默認爲8:2
  • Survivor又被分爲了S0、S1,這兩個的空間大小比例爲1:1

內存分配以及垃圾回收

  1. 對象優先在Eden區進行分配,如果Eden區滿了之後會觸發一次Minor GC
  2. Minor GC之後從Eden存活下來的對象將會被移動到S0區域,當S0內存滿了之後又會被觸發一次Minor GC,S0區存活下來的對象會被移動到S1區,S0區空閒;S1滿了之後在Minor GC,存活下來的再次移動到S0區,S1區空閒,這樣反反覆覆GC,每GC一次,對象的年齡就漲一歲,默認達到15歲之後就會進入老年代,對於晉身到老年代的年齡閾值可以通過參數 -XX:MaxTenuringThreshold設置
  3. 在Minor GC之後需要的發送晉身到老年代的對象沒有空間安置,那麼就會觸發Full GC (這步非絕對,視垃圾回收器決定)

Minor GC和Full GC的區別:Minor GC是指發生在新生代的垃圾收集行爲,由於對象優先在Eden區分配,並且很多對象都是朝生夕死,所以觸發的頻率相對較高;由於採用的複製算法,所以一般回收速度非常快。Full GC是指發生在老年代的垃圾收集行爲,Full GC的速度一般會比Minor GC慢10倍以上;所以不能讓JVM頻繁的發生Full GC

爲了能夠更好的適應不同程序的內存情況,JVM也不一定要求必須達到年齡15歲才能晉身到老年代,如果在Survivor區中相同年齡的所有對象大小總和大於Survivor區空間的一半,年齡大於或者等於這個年齡的對象將會直接進入到老年代

Full GC觸發條件

  • 代碼中調用 System.gc()
  • 老年代空間不足/滿了
  • 持久區空間不足/滿了

注意:大對象會直接在老年代分配內存,可以通過參數-XX:PretenureSizeThreshold控制對象的大小,通常遇到的大對象是很長的字符串或者數組,如果分配了一大羣大對象只是臨時使用,生命很短暫,那麼就會頻繁的發生Full GC,但是此時的新生代的空間還有空閒;寫代碼的時候,這種情況應該避免,特別是在創建數組的時候要當心

「空間擔保」

在新生代發生Minor GC的時候,JVM會先檢查老年代中可分配的連續空間是否大於新生代所有對象的總和,如果大於,那麼本次Minor GC就可以安全的執行;如果不大於,那麼JVM會先去檢查參數HandlePromotionFailure設置值是否允許空間擔保失敗,如果允許,JVM會繼續檢查老年代可分配的連續空間是否大於歷次晉升到老年代對象的平均大小,如果大於,儘管這次Minor GC是有風險的,JVM也會嘗試一次Minor GC;如果不允許擔保失敗,那麼JVM直接進行Full GC

雖然擔保有可能會失敗,導致饒一圈才能進行GC,但是還是建議把這個參數打開,可以避免JVM頻繁的Full GC

垃圾回收器概覽

從上圖可以看出:

  • 新生代可以使用的垃圾回收器:Serial、ParNew、Parallel Scavenge
  • 老年代可以適用的垃圾回收器:CMS、Serial Old、Parallel Old
  • G1回收器適用於新生代和老年代
  • 相互之間有連線的表示可以配合使用

CMS和Serial Old同爲老年代回收器,爲何相互會有連線呢?

Serial收集器

這是個單線程收集器,發展歷史最悠久的收集器,當它在進行垃圾收集工作的時候,其他線程都必須暫停直到垃圾收集結束(Stop The World)。

雖然Serial收集器存在Stop The World的問題,但是在並行能力較弱的單CPU環境下往往表現優於其他收集器;因爲它簡單而高效,沒有多餘的線程交互開銷;Serial對於運行在Client模式下的虛擬機來說是個很好的選擇

使用-XX:+UseSerialGC參數可以設置新生代使用這個Serial收集器

ParNew收集器

ParNew收集器是Serial收集器的多線程版本;除了使用了多線程進行垃圾收集以外,其他的都和Serial一致;它默認開始的線程數與CPU的核數相同,可以通過參數-XX:ParallelGCThreads來設置線程數。

從上面的圖可以看出,能夠與CMS配合使用的收集器,除了Serial以外,就只剩下ParNew,所以ParNew通常是運行在Server模式下的首選新生代垃圾收集器

使用-XX:+UseParNewGC參數可以設置新生代使用這個並行回收器

Parallel Scavenge收集器

Parallel Scavenge收集器依然是個採用複製算法的多線程新生代收集器,它與其他的收集器的不同之處在於它主要關心的是吞吐量,而其他的收集器關注的是儘可能的減少用戶線程的等待時間(縮短Stop The World的時間)。吞吐量=用戶線程執行時間/(用戶線程執行時間+垃圾收集時間),虛擬機總共運行100分鐘,其中垃圾收集花費時間1分鐘,那麼吞吐量就是 99%

「停頓時間越短適合需要和用戶進行交互的程序,良好的響應能夠提升用戶的體驗。而高效的吞吐量可以充分的利用CPU時間,儘快的完成計算任務,所以Parallel Scavenge收集器適用於後臺計算型任務程序。」

-XX:MaxGCPauseMillis可以控制垃圾收集的最大暫停時間,需要注意不要以爲把這個時間設置的很小就可以減少垃圾收集暫用的時間,這可能會導致發生頻繁的GC,反而降低了吞吐量

-XX:GCTimeRatio設置吞吐量大小,參數是取值範圍0-100的整數,也就是垃圾收集佔用的時間,默認是99,那麼垃圾收集佔用的最大時間 1%

-XX:+UseAdaptiveSizePolicy 如果打開這個參數,就不需要用戶手動的控制新生代大小,晉升老年代年齡等參數,JVM會開啓GC自適應調節策略

Serial Old收集器

Serial Old收集器也是個單線程收集器,適用於老年代,使用的是標記-整理算法,可以配合Serial收集器在Client模式下使用。

它可以作爲CMS收集器的後備預案,如果CMS出現Concurrent Mode Failure,則SerialOld將作爲後備收集器。(後面CMS詳細說明)

Parallel Old收集器

Parallel Old收集器可以配合Parallel Scavenge收集器一起使用達到“吞吐量優先”,它主要是針對老年代的收集器,使用的是標記-整理算法。在注重吞吐量的任務中可以優先考慮使用這個組合

-XX:+UseParallelOldGc設置老年代使用該回收器。

XX:+ParallelGCThreads設置垃圾收集時的線程數量。

CMS收集器

CMS收集器是一種以獲取最短回收停頓時間爲目標的收集器,在互聯網網站、B/S架構的中常用的收集器就是CMS,因爲系統停頓的時間最短,給用戶帶來較好的體驗。

-XX:+UseConcMarkSweepGC設置老年代使用該回收器。

-XX:ConcGCThreads設置併發線程數量。

CMS採用的是標記-清除算法,主要分爲了4個步驟:

  • 初始化標記
  • 併發標記
  • 重新標記
  • 併發清除

初始化標記和重新標記這兩個步驟依然會發生Stop The World,初始化標記只是標記GC Root能夠直接關聯到的對象,速度較快,併發標記能夠和用戶線程併發執行;重新標記是爲了修正在併發標記的過程中用戶線程產生的垃圾,這個時間比初始化標記稍長,比並發標記短很多。整個過程請看下圖

「優點」

  • CMS是一款優秀的收集器,它的主要優點:併發收集、低停頓,因此CMS收集器也被稱爲併發低停頓收集器(Concurrent Low Pause Collector)。

「缺點」

  • CMS收集器對CPU資源非常敏感。在併發階段,它雖然不會導致用戶線程停頓,但會因爲佔用了一部分線程(或者說CPU資源)而導致應用程序變慢,總吞吐量會降低。CMS默認啓動的回收線程數是(CPU數量+3)/4,也就是當CPU在4個以上時,併發回收時垃圾收集線程不少於25%的CPU資源,並且隨着CPU數量的增加而下降。但是當CPU不足4個時(比如2個),CMS對用戶程序的影響就可能變得很大,如果本來CPU負載就比較大,還要分出一半的運算能力去執行收集器線程,就可能導致用戶程序的執行速度忽然降低了50%,其實也讓人無法接受。

  • 無法處理浮動垃圾。由於CMS併發清理階段用戶線程還在運行着,伴隨程序運行自然就還會有新的垃圾不斷產生。這一部分垃圾出現在標記過程之後,CMS無法再當次收集中處理掉它們,只好留待下一次GC時再清理掉。這一部分垃圾就被稱爲“浮動垃圾”。也是由於在垃圾收集階段用戶線程還需要運行,那也就還需要預留有足夠的內存空間給用戶線程使用,因此CMS收集器不能像其他收集器那樣等到老年代幾乎完全被填滿了再進行收集,回收閥值可以通過參數-XX:CMSInitiatingoccupancyFraction來設置;如果回收閥值設置的太大,在CMS運行期間如果分配大的對象找不到足夠的空間就會出現“Concurrent Mode Failure”失敗,這時候會臨時啓動SerialOld GC來重新進行老年代的收集,這樣的話停頓的時間就會加長。

  • 標記-清除算法導致的空間碎片 CMS是一款基於“標記-清除”算法實現的收集器,這意味着收集結束時會有大量空間碎片產生。空間碎片過多時,將會給大對象分配帶來很大麻煩,往往出現老年代空間剩餘,但無法找到足夠大連續空間來分配當前對象。爲了解決這個問題CMS提供了一個參數-XX:+UseCMSCompactAtFullCollecion,如果啓用,在Full GC的時候開啓內存碎片整理合並過程,由於內存碎片整理的過程無法並行執行,所以停頓的時間會加長。考慮到每次FullGC都要進行內存碎片合併不是很合適,所以CMS又提供了另一個參數-XX:CMSFullGCsBeforeCompaction來控制執行多少次不帶碎片整理的FullGC之後,來一次帶碎片整理GC

G1收集器

G1是一款面向服務端應用的垃圾回收器。

  • 並行與併發:與CMS類似,充分裏用多核CPU的優勢,G1仍然可以不暫停用戶線程執行垃圾收集工作
  • 分代收集:分代的概念依然在G1保留,當時它不需要和其他垃圾收集器配合使用,可以獨立管理整個堆內存
  • 空間的整合:G1整體上採用的是標記-整理算法,從局部(Region)採用的是複製算法,這兩種算法都意味着G1不需要進行內存碎片整理
  • 可預測的停頓:能夠讓用戶指定在時間片段內,消耗在垃圾收集的時間不超過多長時間。

Region

雖然在G1中依然保留了新生代和老年代的概念,但是採用的是一種完全不同的方式來組織堆內存,它把整個堆內存分割成了很多大小相同的區域(Region),並且新生代和老年代在物理上也不是連續的內存區域,請看下圖:

每個Region被標記了E、S、O和H,其中H是以往算法中沒有的,它代表Humongous,這表示這些Region存儲的是巨型對象,當新建對象大小超過Region大小一半時,直接在新的一個或多個連續Region中分配,並標記爲H。Region區域的內存大小可以通過-XX:G1HeapRegionSize參數指定,大小區間只能是2的冪次方,如:1M、2M、4M、8M

G1的GC模式

  • 新生代GC:與其他新生代收集器類似,對象優先在eden region分配,如果eden region內存不足就會觸發新生代的GC,把存活的對象安置在survivor region,或者晉升到old region
  • 混合GC:當越來越多的對象晉升到了old region,當老年代的內存使用率達到某個閾值就會觸發混合GC,可以通過參數 -XX:InitiatingHeapOccupancyPercent設置閾值百分比,此參數與CMS中 -XX:CMSInitiatingoccupancyFraction的功能類似;混合GC會回收新生代和 「部分老年代內存」,注意是部分老年代而不是全部老年代;G1會跟蹤每個Region中的垃圾回收價值,在用戶指定的垃圾收集時間內優先回收價值最大的region
  • Full GC:如果對象內存分配速度過快,混合GC還未回收完成,導致老年代被填滿,就會觸發一次full gc,G1的full gc算法就是單線程執行的serial old gc,此過程與CMS類似,會導致異常長時間的暫停時間,儘可能的避免full gc.

寫到最後(點關注,不迷路)

文中或許會存在或多或少的不足、錯誤之處,有建議或者意見也非常歡迎大家在評論交流。

最後,請朋友們「不要白嫖我喲」,希望朋友們可以「點贊」,因爲這些就是我分享的全部動力來源🙏



推薦閱讀:


喜歡我可以給我設爲星標哦

好文章,我 “在看”

本文分享自微信公衆號 - 漫話編程(mhcoding)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章