以前根據 Sun 的內存管理白皮書介紹了在 HotSpot JVM 分代算法中的幾個垃圾收集器,本文將介紹 G1 垃圾收集器。算法
G1 的主要關注點在於達到可控的停頓時間,在這個基礎上儘量提升吞吐量,這一點很是重要。數據結構
G1 被設計用來長期取代 CMS 收集器,和 CMS 相同的地方在於,它們都屬於併發收集器,在大部分的收集階段都不須要掛起應用程序。區別在於,G1 沒有 CMS 的碎片化問題(或者說不那麼嚴重),同時提供了更加可控的停頓時間。併發
若是你的應用使用了較大的堆(如 6GB 及以上)並且還要求有較低的垃圾收集停頓時間(如 0.5 秒),那麼 G1 是你絕佳的選擇,是時候放棄 CMS 了。spa
閱讀建議:本文力求用簡單的話介紹清楚 G1 收集器,可是並不會重複介紹每個細節,因此但願讀者瞭解其餘幾個收集器的工做過程,尤爲是 CMS 收集器。線程
首先是內存劃分上,以前介紹的分代收集器將整個堆分爲年輕代、老年代和永久代,每一個代的空間是肯定的。設計
而 G1 將整個堆劃分爲一個個大小相等的小塊(每一塊稱爲一個 region),每一塊的內存是連續的。和分代算法同樣,G1 中每一個塊也會充當 Eden、Survivor、Old 三種角色,可是它們不是固定的,這使得內存使用更加地靈活。
執行垃圾收集時,和 CMS 同樣,G1 收集線程在標記階段和應用程序線程併發執行,標記結束後,G1 也就知道哪些區塊基本上是垃圾,存活對象極少,G1 會先從這些區塊下手,由於從這些區塊能很快釋放獲得很大的可用空間,這也是爲何 G1 被取名爲 Garbage-First 的緣由。
在 G1 中,目標停頓時間很是很是重要,用 -XX:MaxGCPauseMillis=200 指按期望的停頓時間。對象
G1 使用了停頓預測模型來知足用戶指定的停頓時間目標,並基於目標來選擇進行垃圾回收的區塊數量。G1 採用增量回收的方式,每次回收一些區塊,而不是整堆回收。圖片
咱們要知道 G1 不是一個實時收集器,它會盡力知足咱們的停頓時間要求,但也不是絕對的,它基於以前垃圾收集的數據統計,估計出在用戶指定的停頓時間內能收集多少個區塊。內存
注意:G1 有和應用程序一塊兒運行的併發階段,也有 stop-the-world 的並行階段。可是,Full GC 的時候仍是單線程運行的,因此咱們應該儘可能避免發生 Full GC,後面咱們也會介紹何時會觸發 Full GC。工作流
G1 內存佔用
注:這裏不那麼重要。
G1 比 ParallelOld 和 CMS 會須要更多的內存消耗,那是由於有部份內存消耗於簿記(accounting)上,如如下兩個數據結構:
Remembered Sets:每一個區塊都有一個 RSet,用於記錄進入該區塊的對象引用(如區塊 A 中的對象引用了區塊 B,區塊 B 的 Rset 須要記錄這個信息),它用於實現收集過程的並行化以及使得區塊能進行獨立收集。整體上 Remembered Sets 消耗的內存小於 5%。
Collection Sets:將要被回收的區塊集合。GC 時,在這些區塊中的對象會被複制到其餘區塊中,整體上 Collection Sets 消耗的內存小於 1%。
前面囉裏囉嗦說了挺多的,惟一要記住的就是,G1 的設計目標就是盡力知足咱們的目標停頓時間上的要求。
本節介紹 G1 的收集過程,G1 收集器主要包括瞭如下 4 種操做:
一、年輕代收集
二、併發收集,和應用線程同時執行
三、混合式垃圾收集
*、必要時的 Full GC
接下來,咱們進行一一介紹。
首先,咱們來看下 G1 的堆結構:
年輕代中的垃圾收集流程(Young GC):
咱們能夠看到,年輕代收集概念上和以前介紹的其餘分代收集器大差不差的,可是它的年輕代會動態調整。
接下來是 Old GC 的流程(含 Young GC 階段),其實把 Old GC 理解爲併發週期是比較合理的,不要單純地認爲是清理老年代的區塊,由於這一步和年輕代收集也是相關的。下面咱們介紹主要流程:
1.初始標記:stop-the-world,它伴隨着一次普通的 Young GC 發生,而後對 Survivor 區(root region)進行標記,由於該區可能存在對老年代的引用。
由於 Young GC 是須要 stop-the-world 的,因此併發週期直接重用這個階段,雖然會增長 CPU 開銷,可是停頓時間只是增長了一小部分。
2.掃描根引用區:掃描 Survivor 到老年代的引用,該階段必須在下一次 Young GC 發生前結束。
這個階段不能發生年輕代收集,若是中途 Eden 區真的滿了,也要等待這個階段結束才能進行 Young GC。
3.併發標記:尋找整個堆的存活對象,該階段能夠被 Young GC 中斷。
這個階段是併發執行的,中間能夠發生屢次 Young GC,Young GC 會中斷標記過程
4.從新標記:stop-the-world,完成最後的存活對象標記。使用了比 CMS 收集器更加高效的 snapshot-at-the-beginning (SATB) 算法。
Oracle 的資料顯示,這個階段會回收徹底空閒的區塊
5.清理:清理階段真正回收的內存不多。
到這裏,G1 的一個併發週期就算結束了,其實就是主要完成了垃圾定位的工做,定位出了哪些分區是垃圾最多的。
併發週期結束後是混合垃圾回收週期,不只進行年輕代垃圾收集,並且回收以前標記出來的老年代的垃圾最多的部分區塊。
混合垃圾回收週期會持續進行,直到幾乎全部的被標記出來的分區(垃圾佔比大的分區)都獲得回收,而後恢復到常規的年輕代垃圾收集,最終再次啓動併發週期。
到這裏咱們已經說了年輕代收集、併發週期、混合回收週期了,你們要熟悉這幾個階段的工做。
下面咱們來介紹特殊狀況,那就是會致使 Full GC 的狀況,也是咱們須要極力避免的:
1.concurrent mode failure:併發模式失敗,CMS 收集器也有一樣的概念。G1 併發標記期間,若是在標記結束前,老年代被填滿,G1 會放棄標記。
這個時候說明堆須要增長了,或者須要調整併發週期,如增長併發標記的線程數量,讓併發標記儘快結束或者就是更早地進行併發週期,默認是整堆內存的 45% 被佔用就開始進行併發週期。
2.晉升失敗:併發週期結束後,是混合垃圾回收週期,伴隨着年輕代垃圾收集,進行清理老年代空間,若是這個時候清理的速度小於消耗的速度,致使老年代不夠用,那麼會發生晉升失敗。
說明混合垃圾回收須要更迅速完成垃圾收集,也就是說在混合回收階段,每次年輕代的收集應該處理更多的老年代已標記區塊。
3.疏散失敗:年輕代垃圾收集的時候,若是 Survivor 和 Old 區沒有足夠的空間容納全部的存活對象。這種狀況確定是很是致命的,由於基本上已經沒有多少空間能夠用了,這個時候會觸發 Full GC 也是很合理的。
最簡單的就是增長堆大小
4.大對象分配失敗,咱們應該儘量地不建立大對象,尤爲是大於一個區塊大小的那種對象。
看完上面的 Young GC 和 Old GC 等,不少讀者可能仍是很懵的,這裏說幾句不嚴謹的白話文幫助讀者進行理解:
首先,最好不要把上面的 Old GC 當作是一次 GC 來看,而應該當作併發標記週期來理解,雖然它確實會釋放出一些內存。
併發標記結束後,G1 也就知道了哪些區塊是最適合被回收的,那些徹底空閒的區塊會在這這個階段被回收。若是這個階段釋放了足夠的內存出來,其實也就能夠認爲結束了一次 GC。
咱們假設併發標記結束了,那麼下次 GC 的時候,仍是會先回收年輕代,若是從年輕代中獲得了足夠的內存,那麼結束;過了幾回後,年輕代垃圾收集不能知足須要了,那麼就須要利用以前併發標記的結果,選擇一些活躍度最低的老年代區塊進行回收。直到最後,老年代會進入下一個併發週期。
那麼何時會啓動併發標記週期呢?這個是經過參數控制的,下面立刻要介紹這個參數了,此參數默認值是 45,也就是說當堆空間使用了 45% 後,G1 就會進入併發標記週期。
G1 調優的目標是儘可能避免出現 Full GC,其實就是給老年代足夠的空間,或相對更多的空間。
有如下幾點咱們能夠進行調整的方向:
增長堆大小,或調整老年代和年輕代的比例,這個很好理解
增長併發週期的線程數量,其實就是爲了加快併發週期快點結束
讓併發週期儘早開始,這個是經過設置堆使用佔比來調整的(默認 45%)
在混合垃圾回收週期中回收更多的老年代區塊
G1 的很重要的目標是達到可控的停頓時間,因此不少的行爲都以這個目標爲出發點開展的。
咱們經過設置 -XX:MaxGCPauseMillis=N 來指定停頓時間(單位 ms,默認 200ms),若是沒有達到這個目標,G1 會經過各類方式來補救:調全年輕代和老年代的比例,調整堆大小,調整晉升的年齡閾值,調整混合垃圾回收週期中處理的老年代的區塊數量等等。
固然了,調整每一個參數知足了一個條件的同時每每也會引入另外一個問題,好比爲了下降停頓時間,咱們能夠減少年輕代的大小,但是這樣的話就會增長年輕代垃圾收集的頻率。若是咱們減小混合垃圾回收週期處理的老年代區塊數量,雖然能夠更容易知足停頓時間要求,但是這樣就會增長 Full GC 的風險等等。
下面介紹最經常使用也是最基礎的一些參數的設置,涉及到更高級的調優參數設置,請讀者自行參閱其餘資料。
參數介紹:
-XX:+UseG1GC 使用 G1 收集器
-XX:MaxGCPauseMillis=200 指定目標停頓時間,默認值 200 毫秒。
在設置 -XX:MaxGCPauseMillis 值的時候,不要指定爲平均時間,而應該指定爲知足 90% 的停頓在這個時間以內。記住,停頓時間目標是咱們的目標,不是每次都必定能知足的。
-XX:InitiatingHeapOccupancyPercent=45 整堆使用達到這個比例後,觸發併發 GC 週期,默認 45%。
若是要下降晉升失敗的話,一般能夠調整這個數值,使得併發週期提早進行
-XX:NewRatio=n
老年代/年輕代,默認值 2,即 1/3 的年輕代,2/3 的老年代老年代/年輕代,默認值 2,即 1/3 的年輕代,2/3 的老年代。不要設置年輕代爲固定大小,不然:G1 再也不須要知足咱們的停頓時間目標,不能再按需擴容或縮容年輕代大小
-XX:SurvivorRatio=n
Eden/Survivor,默認值 8,這個和其餘分代收集器是同樣的
-XX:MaxTenuringThreshold =n
從年輕代晉升到老年代的年齡閾值,也是和其餘分代收集器同樣的
-XX:ParallelGCThreads=n
並行收集時候的垃圾收集線程數
-XX:ConcGCThreads=n
併發標記階段的垃圾收集線程數,增長這個值可讓併發標記更快完成,若是沒有指定這個值,JVM 會經過如下公式計算獲得ConcGCThreads=(ParallelGCThreads + 2) / 4^3
-XX:G1ReservePercent=n
堆內存的預留空間百分比,默認 10,用於下降晉升失敗的風險,即默認地會將 10% 的堆內存預留下來。
-XX:G1HeapRegionSize=n
每個 region 的大小,默認值爲根據堆大小計算出來,取值 1MB~32MB,這個咱們一般指定整堆大小就行了。