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

看完文章,若是以爲還能夠,也請各位小夥伴點個「贊👍」,謝謝🙏

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

在開始介紹垃圾收集器前,咱們能夠劇透幾點:java

  • 根據不一樣分代的特色,收集器可能不一樣。有些收集器能夠同時用於新生代和老年代,而有些時候,則須要分別爲新生代或老年代選用合適的收集器。通常來講,新生代收集器的收集頻率較高,應選用性能高效的收集器;而老年代收集器收集次數相對較少,對空間較爲敏感,應當避免選擇基於複製算法的收集器。
  • 在垃圾收集執行的時刻,應用程序須要暫停運行
  • 能夠串行收集,也能夠並行收集。
  • 若是能作到併發收集(應用程序沒必要暫停),那絕對是很妙的事情。
  • 若是收集行爲可控,那也是很妙的事情。
  • 默認收集器算法

    • jdk1.7,1.8 默認垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)
    • jdk1.9 默認垃圾收集器G1

但願你們帶着下面的問題進行閱讀,有目標的閱讀,可能收穫更多。

  1. 爲何沒有一種牛逼的收集器像銀彈同樣適配全部場景?
  2. CMS和G1的對比,你知道他兩的區別嗎?
  3. 爲何CMS只能用做老年代收集器,而不能應用在新生代的收集?
  4. 爲何JVM的分代年齡是15?而不是16,20之類的呢?
  5. 「動態對象年齡斷定」裏有個「天坑」哦,是啥坑呢?

1 垃圾收集器

GC線程與應用線程保持相對獨立,當系統須要執行垃圾回收任務時,先中止工做線程,而後命令GC線程工做。以串行模式工做的收集器,稱爲串行收集器(即Serial Collector)。與之相對的是以並行模式工做的收集器,稱爲並行收集器(即Paraller Collector)segmentfault

1.1 串行收集器:Serial

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

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

是針對新生代的垃圾回收器,基於標記-複製算法多線程

1.2 並行收集器:ParNew

並行收集器充分利用了多處理器的優點,採用多個GC線程並行收集。可想而知,多條GC線程執行顯然比只使用一條GC線程執行的效率更高。通常來講,與串行收集器相比,在多處理器環境下工做的並行收集器可以極大地縮短Stop-the-world時間。併發

針對新生代的垃圾回收器,標記-複製算法,能夠當作是Serial的多線程版本oracle

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

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

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

腦圖太大,如需高清完整大圖,請留言告知。
相關文章
相關標籤/搜索