java虛擬機——垃圾收集器與回收策略

本文主要介紹常見的java垃圾收集器、如何閱讀jvm日誌和虛擬機中內存的分配策略。

java虛擬機——垃圾收集器與回收策略


一、垃圾收集器

垃圾收集器可以是看做內存回收的具體實現。Java虛擬機規範對垃圾收集器並未做任何規定,所以不同廠商、不同版本虛擬機提供的垃圾收集器有很大不同。版本主要討論HotSpot虛擬機的垃圾回收器。

java虛擬機——垃圾收集器與回收策略


本圖為常見的七種垃圾回收器,上半部分為年輕代的回收器,下半部分為老年代的回收器,G1比較全能,上可回收年輕代,下可處理老年代。接下來我們對各收集器進行介紹。

年輕代收集器

1.Serial收集器

這是一個古老的垃圾回收器,使用複製算法。Serial是一個單線程的收集器,這個“單線程”並不是僅僅說明它只會使用一個CPU或一條線程去完成垃圾收集工作,更重要的是在其進行垃圾回收時需要暫停所有其他進程來進行垃圾回收工作(Stop The World)。

Stop The World會暫停全部線程,給用戶帶來不良的體驗,虛擬機開發團隊不斷的努力消除停頓時間,我們也看到了一個個的優秀回收器出現,停頓時間也越來越斷,但仍無法完全消除。

與其他垃圾回收器相比:Serial簡單而高效,對於限定單個CPU的環境來說,Serial不需要進行線程交互,專心撿垃圾吃,效率極高,一般是Client模式下一個很好的選擇。

2.ParNew收集器

ParNew是Serial的多線程版本,在GC時使用多線程進行回收,除此之外,包括所有控制參數,收集算法,Stop The World,對象分配規則,回收策略等都與Serail收集器一致。

但ParNew是Server模式下虛擬機的首選新生代服務器,因為 Parallel收集器無法與CMS配合使用,而在多核CPU的情況下Serial的效率不如ParNew。

並行(Parallel):多條垃圾收集線程並行工作,用戶線程在一旁等待。

併發(Concurrent):用戶線程與垃圾回收線程同時執行,可能交替執行,用戶程序可能和垃圾收集線程運行在不同的CPU上。

3.Parallel Scavenge收集器

Parallel Scavenge是一個新生代的收集器,使用複製算法,也是一個並行多線程的收集器,不過Parallel Scavenge的目的是達到一個可控制的吞吐量(Throughput)。

吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾回收時間)。停頓時間越短越適合需要與客戶交互的程序,高吞吐量可以高效運用CPU時間。

兩個參數:-XX:MaxGCPauseMillis設置最大停頓時間,-XX:GCTimeRation設置吞吐量的大小。但並不是停頓時間越短越好,如果時間過斷,會導致頻繁出發垃圾回收,吞吐量也會隨之下降。

另外,Parallel Scavenge收集器還能根據情況自動調節策略,對於對收集器不太瞭解的可以開啟PS收集器的自動調節功能。-XX:+UseAdaptiveSizePolicy

老年代收集器

1.Serial Old收集器

是Serial的老年代版本,使用“標記—整理”算法。這個收集器同樣也是單線程的,也是給Client模式下的虛擬機使用的。

不過,Serial Old還有其他兩個用途:一、JDK1.5之配合Parallel Scavenge使用 ;二、作為CMS的後備方案,在併發收集發生Concurrent Mode Failure時使用。

2.Parallel Old收集器

是Parallel Scavenge收集器的老年代版本。使用多線程的標記—整理算法。在JDK1.6中提供,在此之前Parallel Scavenge只能配合Serial Old使用,但Serial在服務端性能並不優秀,所以並不能發揮Parallel Scavenge吞吐量最大化的效果。

直到Parallel Old出現後,“吞吐量優先”收集器才有了名符其實的組合,在注重吞吐量及CPU資源敏感的場合都可以考慮Parallel Scavenge+Parallel Old組合。

3.CMS收集器(Concurrent Mark Sweep)

是一種獲取最短回收停頓時間為目的的收集器。使用標記—清除算法。適用於注重相應時間的互聯網站或B/S系統的服務端。

標記過程:

  • 初始標記(initial mark)
  • 併發標記(concurrent mark)
  • 重新標記(remark)
  • 併發清除(concurrent sweep)

初始標記和併發標記仍需要“stop the world”,不過這兩個過程與併發標記相比,所用時間都極短。但用時較長的併發標記和併發清除都可以與用戶進程同時進行。

缺點:

  1. 對CPU資源十分敏感,雖不會導致用戶線程停頓,但會因佔用一部分線程使應用變慢,吞吐量下降。默認啟動回收線程為(cpu數量+3)/4,至少使用25%的資源,對於少於4核的處理器簡直是災難。
  2. 無法處理浮動垃圾,在CMS清除的過程中用戶程序仍在運行,仍會產生垃圾,會在下次GC時清除,需要預留足夠空間給用戶線程使用。如果預留內存無法滿足要求,則會出現Concurrent Mode Failure,需要臨時使用Serial Old進行回收,這樣就會降低效率
  3. 標記清除本身的缺點,大量的碎片化空間會提前出發GC,解決方法是在進行Full GC前進行一次壓縮,如果仍不夠用再進行Full GC

G1收集器 (Garbage-First)

G1是面向服務端的垃圾回收器,出現的意義是替換掉JDK1.5發佈的CMS收集器。

特點:

  • 並行與併發:充分利用CPU,多核環境,使用多CPU來減少Stop The World時間。
  • 分代收集:在G1中分代收集仍被保留,不需要與其他處理器配合。
  • 空間整理:與CMS的“標記—清除”不同,G1從整體上看是基於“標記—整理”,但從局部看是使用“複製算法”,避免產生內存碎片。
  • 可預測停頓:追求低停頓,並可以建立可預測的停頓時間模型。

G1收集器的堆內存劃分與其他收集器不同,會將堆劃分成多個大小相等的獨立區域(Region),在Region中會有年輕代和老年代,G1會優先回收價值大的Region,而不是對整個堆進行回收。虛擬機一般會使用Remembered Set避免進行全堆掃描,G1中每個Region都會有一個Remebered Set。

運作過程:

  • 初始標記(Initial Marking)
  • 併發標記(Concurrent Marking)
  • 最終標記(Final Marking)
  • 篩選回收(Live Date Counting and Evacuation)

二、理解GC日誌

jdk1.7

[GC [PSYoungGen: 7926K->480K(153600K)] 7926K->480K(502784K), 0.0014400 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
[Full GC [PSYoungGen: 480K->0K(153600K)] [ParOldGen: 0K->318K(349184K)] 480K->318K(502784K) [PSPermGen: 2867K->2866K(21504K)], 0.0101200 secs] [Times: user=0.03 sys=0.00, real=0.01 secs]

jdk1.8

[GC (System.gc()) [PSYoungGen: 5263K->608K(153088K)] 5263K->616K(502784K), 0.0015586 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
[Full GC (System.gc()) [PSYoungGen: 608K->0K(153088K)] [ParOldGen: 8K->404K(349696K)] 616K->404K(502784K), [Metaspace: 3141K->3141K(1056768K)], 0.0048705 secs] [Times: user=0.03 sys=0.00, real=0.00 secs]
Heap

[GC和[Full GC是用來區分垃圾回收的類型,而不是區分年輕代GC還是老年代GC,Full GC代表發生了“Stop The World”。(System.gc())代表調用System.gc()觸發垃圾回收。年輕代GC是Minor GC,老年代的GC是Major GC,Full GC是對整個堆內存進行GC。

PSYoungGen、ParOldGen、PSPermGen代表的是垃圾回收的內存區域,與使用的垃圾回收器相關。

java虛擬機——垃圾收集器與回收策略

在jdk1.8中永久代全部換成了元空間Metaspace~

在方括號內部的5263K->608K是指該區域內存已用容量->該區域GC內存使用量,在方括號外的5263K->616K是指GC前java堆使用容量->GC後堆內存使用量。0.0015586 secs代表本次GC消耗時間,單位是秒。

[Times: user=0.03 sys=0.00, real=0.00 secs] ,分別是用戶態消耗CPU時間、內核態消費CPU時間和操作開始到操作結束經過的牆鍾時間(Wall Clock Time);牆鍾時間包括各種非運算時間,IO、線程阻塞,CPU耗時則不包括這些,但如果是多線程或者是多核則會將這些時間疊加,所以CPU時間大於真實時間也很正常。

三、內存分配策略

java虛擬機自動管理內存歸結為兩個問題:給對象分配內存和回收分配給對象的內存。

三個新生代的收集器都是使用複製算法。複製算法中將內存分為三個區域,一個Eden區,兩個Survivor區,默認比例是8:1:1,可以使用-XX:SurvivorRatio=8來進行配置,代表Eden:Survivor=8:1。接下來以Serial/Serial old為例進行講述。

對象優先在Eden區進行分配,如果Eden區空間不夠會進行一次Minor GC。

java虛擬機——垃圾收集器與回收策略


Minor GCMinor GC時,會將存活的對象都放入到另一個Survivor(右)中,新來的對象會繼續在eden中進行分配,放不下的時候進行MinorGC,把存活的對象放入另一個不用的Survivor(左)中。

長期存活的對象進入老年代存活的對象都會有一個年齡,記錄經歷了幾輪GC,當達到一定年齡後就會將對象放入到老年代中。默認晉升年齡閾值為15,可以使用-XX:MaxTenuringThreshold=15來進行配置。

大對象進入老年代如果新來的是個大對象,可能會直接進入老年代,大對象在年輕代會佔據連續的空間,尤其是的短命的大對象,容易引起內存還剩不少時就出發垃圾回收。長壽命的大對象也有問題,在Eden和Survivor之間來回複製會發生大量的內存複製,這會大大降低複製算法的效率。所以一般來將,大對象直接進入老年代。對於Serial和ParNew收集器,可以使用-XX:PretenureSizeThreshold=121241來配置多大的對象是大對象,單位是k。

Survivor中同齡對象超過空間一半進入老年代當Survivor中1歲啦,2歲啦的對象超過一半時會直接進入老年代。

空間分配擔保在發生Minor GC之前,虛擬機會先檢查老年代最大可用連續空間是否大於新生代所有對象總空間,如果條件成立,則表明本次GC是安全的,即使全都進了老年代還裝的下!

如果不成立,查看+XX:-HandlePromotionFailure設置是否擔保失敗。如果允許,需要判斷老年代連續空間是否大於歷次晉升到老年代的平均值

,那麼嘗試Minor GC,儘管存在風險,如果小於未配置這個參數,需要進行一次Full GC。

風險指的是複製算法收集存活的對象如果大於Survivor能容納的對象,會直接進入老年代,如果老年代沒有足夠的空間就會出發Full GC進行垃圾回收。儘管如此,只要連續空間大於平均值,就有很大的概率不會進入Full GC,這樣就能有效的提高性能。

四、總結

本文介紹了常見的集中垃圾回收器、GC日誌的理解和Java的內存分配和回收策略,這些都是jvm知識體系中很重要的內容,有必要我們去仔細研究!

原文:https://icdream.github.io/2019/01/21/jvm04/#more


分享到:


相關文章: