【JVM從小白學成大佬】5.垃圾收集器及內存分配策略


前面介紹了垃圾回收算法,接下來我們介紹垃圾收集器和內存分配的策略。有沒有一種牛逼的收集器像銀彈一樣適配所有場景?很明顯,不可能有,不然我也沒必要單獨搞一篇文章來介紹垃圾收集器了。熟悉不同收集器的優缺點,在實際的場景中靈活運用,才是王道。

在開始介紹垃圾收集器前,我們可以劇透幾點:

  • 根據不同分代的特點,收集器可能不同。有些收集器可以同時用於新生代和老年代,而有些時候,則需要分別為新生代或老年代選用合適的收集器。一般來說,新生代收集器的收集頻率較高,應選用性能高效的收集器;而老年代收集器收集次數相對較少,對空間較為敏感,應當避免選擇基於復制算法的收集器。
  • 在垃圾收集執行的時刻,應用程序需要暫停運行
  • 可以串行收集,也可以並行收集。
  • 如果能做到並發收集(應用程序不必暫停),那絕對是很妙的事情。
  • 如果收集行為可控,那也是很妙的事情。

希望大家帶着下面的問題進行閱讀,有目標的閱讀,可能收獲更多。

  1. 為什么沒有一種牛逼的收集器像銀彈一樣適配所有場景?
  2. CMS和G1的對比,你知道他兩的區別嗎?
  3. 為什么CMS只能用作老年代收集器,而不能應用在新生代的收集?
  4. 為什么JVM的分代年齡是15?而不是16,20之類的呢?
  5. “動態對象年齡判定”里有個“天坑”哦,是啥坑呢?

1 垃圾收集器

GC線程與應用線程保持相對獨立,當系統需要執行垃圾回收任務時,先停止工作線程,然后命令GC線程工作。以串行模式工作的收集器,稱為串行收集器(即Serial Collector)。與之相對的是以並行模式工作的收集器,稱為並行收集器(即Paraller Collector)

1.1 串行收集器:Serial

串行收集器采用單線程方式進行收集,且在GC線程工作時,系統不允許應用線程打擾。此時,應用程序進入暫停狀態,即Stop-the-world。

Stop-the-world暫停時間的長短,是度量一款收集器性能高低的重要指標。

是針對新生代的垃圾回收器,基於標記-復制算法

1.2 並行收集器:ParNew

並行收集器充分利用了多處理器的優勢,采用多個GC線程並行收集。可想而知,多條GC線程執行顯然比只使用一條GC線程執行的效率更高。一般來說,與串行收集器相比,在多處理器環境下工作的並行收集器能夠極大地縮短Stop-the-world時間。

針對新生代的垃圾回收器,標記-復制算法,可以看成是Serial的多線程版本

1.3 吞吐量優先收集器:Parallel Scavenge

針對新生代的垃圾回收器,標記-復制算法,和ParNew類似,但更注重吞吐率。在ParNew的基礎上演化而來的Parallel Scanvenge收集器被譽為“吞吐量優先”收集器。吞吐量就是CPU用於運行用戶代碼的時間與CPU總消耗時間的比值,即吞吐量=運行用戶代碼時間 /(運行用戶代碼時間 + 垃圾收集時間)。如虛擬機總運行了100分鍾,其中垃圾收集花掉1分鍾,那吞吐量就是99%。

Parallel Scanvenge收集器在ParNew的基礎上提供了一組參數,用於配置期望的收集時間或吞吐量,然后以此為目標進行收集。

通過VM選項可以控制吞吐量的大致范圍:

  • -XX:MaxGCPauseMills:期望收集時間上限。用來控制收集對應用程序停頓的影響。
  • -XX:GCTimeRatio:期望的GC時間占總時間的比例,用來控制吞吐量。
  • -XX:UseAdaptiveSizePolicy:自動分代大小調節策略。

但要注意停頓時間與吞吐量這兩個目標是相悖的,降低停頓時間的同時也會引起吞吐的降低。因此需要將目標控制在一個合理的范圍中。

1.4 Serial Old收集器

Serial Old是Serial收集器的老年代版本,單線程收集器,使用標記-整理算法。這個收集器的主要意義也是在於給Client模式下的虛擬機使用。

1.5 Parallel Old收集器

Parallel Old是Parallel Scanvenge收集器的老年代版本,多線程收集器,使用標記-整理算法

1.6 CMS收集器

CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器

CMS收集器僅作用於老年代的收集,是基於標記-清除算法的,它的運作過程分為4個步驟:

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

其中,初始標記、重新標記這兩個步驟仍然需要Stop-the-world。初始標記僅僅只是標記一下GC Roots能直接關聯到的對象,速度很快,並發標記階段就是進行GC Roots Tracing的過程,而重新標記階段則是為了修正並發標記期間因用戶程序繼續運作而導致標記產生變動的那一部分對象的標記記錄,這個階段的停頓時間一般會比初始階段稍長一些,但遠比並發標記的時間短。

CMS以流水線方式拆分了收集周期,將耗時長的操作單元保持與應用線程並發執行。只將那些必需STW才能執行的操作單元單獨拎出來,控制這些單元在恰當的時機運行,並能保證僅需短暫的時間就可以完成。這樣,在整個收集周期內,只有兩次短暫的暫停(初始標記和重新標記)達到了近似並發的目的

CMS收集器優點:並發收集、低停頓。

CMS收集器缺點

  • CMS收集器對CPU資源非常敏感。
  • CMS收集器無法處理浮動垃圾(Floating Garbage)。
  • CMS收集器是基於標記-清除算法,該算法的缺點都有。

CMS收集器之所以能夠做到並發,根本原因在於采用基於“標記-清除”的算法並對算法過程進行了細粒度的分解。前面篇章介紹過標記-清除算法將產生大量的內存碎片這對新生代來說是難以接受的,因此新生代的收集器並未提供CMS版本。

1.7 G1收集器

G1重新定義了堆空間,打破了原有的分代模型,將堆划分為一個個區域。這么做的目的是在進行收集時不必在全堆范圍內進行,這是它最顯著的特點。區域划分的好處就是帶來了停頓時間可預測的收集模型:用戶可以指定收集操作在多長時間內完成。即G1提供了接近實時的收集特性。

G1與CMS的特征對比如下:

特征 G1 CMS
並發和分代
最大化釋放堆內存
低延時
吞吐量
壓實
可預測性
新生代和老年代的物理隔離

G1具備如下特點:

  • 並行與並發:G1能充分利用多CPU、多核環境下的硬件優勢,使用多個CPU來縮短Stop-the-world停頓的時間,部分其他收集器原來需要停頓Java線程執行的GC操作,G1收集器仍然可以通過並發的方式讓Java程序繼續運行。
  • 分代收集
  • 空間整合:與CMS的標記-清除算法不同,G1從整體來看是基於標記-整理算法實現的收集器,從局部(兩個Region之間)上來看是基於“復制”算法實現的。但無論如何,這兩種算法都意味着G1運作期間不會產生內存空間碎片,收集后能提供規整的可用內存。這種特性有利於程序長時間運行,分配大對象時不會因為無法找到連續內存空間而提前觸發下一次GC
  • 可預測的停頓:這是G1相對於CMS的一個優勢,降低停頓時間是G1和CMS共同的關注點。

在G1之前的其他收集器進行收集的范圍都是整個新生代或者老年代,而G1不再是這樣。在堆的結構設計時,G1打破了以往將收集范圍固定在新生代或老年代的模式,G1將堆分成許多相同大小的區域單元,每個單元稱為Region。Region是一塊地址連續的內存空間,G1模塊的組成如下圖所示:

G1堆的Region布局.png

G1收集器將整個Java堆划分為多個大小相等的獨立區域(Region),雖然還保留有新生代和老年代的概念,但新生代和老年代不再是物理隔離的了,它們都是一部分Region(不需要連續)的集合。G1收集器之所以能建立可預測的停頓時間模型,是因為它可以有計划地避免在整個Java堆中進行全區域的垃圾收集。G1會通過一個合理的計算模型,計算出每個Region的收集成本並量化,這樣一來,收集器在給定了“停頓”時間限制的情況下,總是能選擇一組恰當的Regions作為收集目標,讓其收集開銷滿足這個限制條件,以此達到實時收集的目的。

對於打算從CMS或者ParallelOld收集器遷移過來的應用,按照官方 的建議,如果發現符合如下特征,可以考慮更換成G1收集器以追求更佳性能:

  • 實時數據占用了超過半數的堆空間;
  • 對象分配率或“晉升”的速度變化明顯;
  • 期望消除耗時較長的GC或停頓(超過0.5——1秒)。

原文如下:
Applications running today with either the CMS or the ParallelOld garbage collector would benefit switching to G1 if the application has one or more of the following traits.

  • More than 50% of the Java heap is occupied with live data.
  • The rate of object allocation rate or promotion varies significantly.
  • Undesired long garbage collection or compaction pauses (longer than 0.5 to 1 second)

G1收集的運作過程大致如下:

  • 初始標記(Initial Marking):僅僅只是標記一下GC Roots能直接關聯到的對象,並且修改TAMS(Next Top at Mark Start)的值,讓下一階段用戶程序並發運行時,能在正確可用的Region中創建新對象,這階段需要停頓線程,但耗時很短
  • 並發標記(Concurrent Marking):是從GC Roots開始堆中對象進行可達性分析,找出存活的對象,這階段耗時較長,但可與用戶程序並發執行。
  • 最終標記(Final Marking):是為了修正並發標記期間因用戶程序繼續運作而導致標記產生變動的那一部分標記記錄,虛擬機將這段時間對象變化記錄在線程Remembered Set Logs里面,最終標記階段需要把Remembered Set Logs的數據合並到Remembered Set中,這階段需要停頓線程,但是可並行執行
  • 篩選回收(Live Data Counting and Evacuation):首先對各個Region的回收價值和成本進行排序,根據用戶所期望的GC停頓時間來制定回收計划。這個階段也可以做到與用戶程序一起並發執行,但是因為只回收一部分Region,時間是用戶可控制的,而且停頓用戶線程將大幅提高收集效率。

我們可以看下官方文檔對G1的展望(這段英文描述比較簡單,我就不翻譯了):

Future:
G1 is planned as the long term replacement for the Concurrent Mark-Sweep Collector (CMS). Comparing G1 with CMS, there are differences that make G1 a better solution. One difference is that G1 is a compacting collector. G1 compacts sufficiently to completely avoid the use of fine-grained free lists for allocation, and instead relies on regions. This considerably simplifies parts of the collector, and mostly eliminates potential fragmentation issues. Also, G1 offers more predictable garbage collection pauses than the CMS collector, and allows users to specify desired pause targets.

2 內存分配策略

對象的內存分配,往大方向上講,就是在上分配(但也可能經過JIT編譯后被拆散為標量類型並間接地棧上分配),對象主要分配在新生代的Eden區上,如果啟動了本地線程分配緩沖,將按線程優先在TLAB上分配。少數情況下可能會直接分配在老年代中。

2.1 對象優先在Eden分配

大多數情況下,對象在新生代Eden區中分配。當Eden區沒有足夠空間進行分配時,虛擬機將發起一次Minor GC(前面篇章中有介紹過Minor GC)。但也有一種情況,在內存擔保機制下,無法安置的對象會直接進到老年代。

2.2 大對象直接進入老年代

大對象時指需要大量連續內存空間的Java對象,最典型的大對象就是那種很長的字符串以及數組。

虛擬機提供了一個-XX:PretenureSizeThreshold參數,令大於這個設置值的對象直接在老年代分配。目的就是避免在Eden區及兩個Survivor區之間發生大量的內存復制。

2.3 長期存活的對象將進入老年代

虛擬機給每個對象定義了一個對象年齡(Age)計數器。如果對象在Eden出生並經過第一次Minor GC后仍然存活,並且能被Survivor容納的話,將被移動到Survivor空間中,並且對象年齡設為1 。對象在Survivor區中沒經過一次Minor GC,年齡就加1歲,當年齡達到15歲(默認值),就會被晉升到老年代中。

對象晉升老年代的年齡閾值,可以通過參數-XX:MaxTenuringThreshold設置。

接下來我們來回答為什么JVM的分代年齡為什么是15?而不是16,20之類的呢?

真的不是為什么不能是其它數(除了15),着實是臣妾做不到啊!

事情是這樣的,HotSpot虛擬機的對象頭其中一部分用於存儲對象自身的運行時數據,如哈希碼(HashCode)、GC分代年齡、鎖狀態標志、線程持有的鎖、偏向線程ID、偏向時間戳等,這部分數據的長度在32位和64位的虛擬機(未開啟壓縮指針)中分別為32bit和64bit,官方稱它為“Mark word”。

例如,在32位的HotSpot虛擬機中,如果對象處於未被鎖定的狀態下,那么Mark Word的32bit空間中25bit用於存儲對象哈希碼,4bit用於存儲對象分代年齡,2bit用於存儲鎖標志位,1bit固定為0 。

明白是什么原因了嗎?對象的分代年齡占4位,也就是0000,最大值為1111也就是最大為15,而不可能為16,20之類的了。

2.4 動態對象年齡判定

為了能更好的適應不同程序的內存狀況,虛擬機並不是永遠地要求兌現過的年齡必須達到了MaxTenuringThreshold才能晉升老年代。

滿足如下條件之一,對象能晉升老年代:

  • 1.對象的年齡達到了MaxTenuringThreshold(默認15)能晉升老年代。
  • 2.如果在Survivor空間中相同年齡所有對象大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的對象就可以直接進入老年代,無須等到MaxTenuringThreshold中要求的年齡。

很多文章都只是注意到了上面描述的情況(包括阿里中間件公眾號發的一篇文章里也只是這么簡單的介紹,當時給它們后台留過言說明情況),但如果只是這么認識的話,會發現在實際的內存回收中有悖於此條規定。

舉個小栗子,如如對象年齡5的占30%,年齡6的占36%,年齡7的占34%,按那兩個標准,對象是不能進入老年代的,但Survivor都已經100%了啊

大家可以關注這個參數TargetSurvivorRatio,目標存活率,默認為50%。大致意思就是說年齡從小到大累加,如加入某個年齡段(如栗子中的年齡6)后,總占用超過Survivor空間*TargetSurvivorRatio的時候,從該年齡段開始及大於的年齡對象就要進入老年代(即栗子中的年齡6對象,就是年齡6和年齡7晉升到老年代)。動態對象年齡判斷,主要是被TargetSurvivorRatio這個參數來控制。而且算的是年齡從小到大的累加和,而不是某個年齡段對象的大小。

2.5 空間分配擔保

在發生Minor GC之前,虛擬機會先檢查老年代最大可用的連續空間是否大於新生代所有對象總空間,如果這個條件成立,那么Minor GC可以確保是安全的。如果不成立,則虛擬機會查看HandlePromotionFailure設置值是否允許擔保失敗。如果允許,那么會繼續檢查老年代最大可用的連續空間是否大於歷次晉升到老年代對象的平均大小,如果大於,將嘗試着進行一次Minor GC,盡管這次Minor GC是有風險的;如果小於,或者HandlePromotionFailure設置不允許冒險,那這時也要改為進行一次Full GC

上面說的風險是什么呢?我們知道,新生代使用復制收集算法,但為了內存利用率,只使用其中一個Survivor空間來作為輪換備份,因此當出現大量對象在Minor GC后仍然存活的情況(最極端的情況就是內存回收后新生代中所有對象都存活),就需要老年代進行分配擔保,把Survivor無法容納的對象直接進入老年代。

3.總結腦圖

內存分配策略.png

腦圖太大,如需高清完整大圖,請留言告知。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM