1. 程式人生 > >讀書筆記 ---- 《深入理解Java虛擬機器》---- 第3篇:垃圾收集器

讀書筆記 ---- 《深入理解Java虛擬機器》---- 第3篇:垃圾收集器

上一篇:垃圾回收演算法:https://blog.csdn.net/pcwl1206/article/details/84061589

本篇文章轉發自:https://blog.csdn.net/chjttony/article/details/7883748

第3篇:垃圾收集器

1  Serial收集器

2  ParNew收集器

3  Parallel  Scavenge收集器

4  Serial  Old收集器

5  Parallel  Old收集器

6  CMS收集器

7  G1收集器

8  需要注意的幾點

8.1  物件優先在Eden分配

8.2  大物件直接進入老年代

8.3  長期存活的物件將進入老年代

8.4  動態物件年齡判斷

8.5  空間分配擔保

G1垃圾收集器介紹

G1 總覽

G1 工作流程

G1 引數配置和最佳實踐

各種GC的觸發時機(When)

GC型別

FULL GC觸發條件詳解

參考及推薦


 


Java堆記憶體被劃分為新生代和年老代兩部分,新生代主要使用複製和標記-清除垃圾回收演算法,年老代主要使用標記-整理垃圾回收演算法,因此java虛擬中針對新生代和年老代分別提供了多種不同的垃圾收集器,JDK1.7中Sun HotSpot虛擬機器的垃圾收集器如下:

說明:圖中兩兩連線的收集器都是可以搭配使用的。

再開始講解具體的垃圾收集器之前,先明確一個觀點:雖然我們是在對各個收集器進行比較,但並非為了挑出一個最好的收集器。因為直到現在為止還沒有最好的收集器出現,更加沒有萬能的收集器,所以我們選擇的只是對具體應用最合適的收集器。

  • 設計上的權衡

往下看之前,我們需要先分清楚這裡的兩個概念:併發和並行

  • 並行:多個垃圾回收執行緒同時工作,而不是隻有一個垃圾回收執行緒在工作
  • 併發:垃圾回收執行緒和應用程式執行緒同時工作,應用程式不需要掛起

在設計或選擇垃圾回收演算法的時候,我們需要作出以下幾個權衡:

  • 序列 vs 並行

    序列收集的情況,即使是多核 CPU,也只有一個核心參與收集。使用並行收集器的話,垃圾收集的工作將分配給多個執行緒在不同的 CPU 上同時進行。並行可以讓收集工作更快,缺點是帶來的複雜性和記憶體碎片問題。

  • 併發 vs Stop-the-world

    當 stop-the-world 垃圾收集器工作的時候,應用將完全被掛起。與之相對的,併發收集器在大部分工作中都是併發進行的,也許會有少量的 stop-the-world。

    stop-the-world 垃圾收集器比並發收集器簡單很多,因為應用掛起後堆空間不再發生變化,它的缺點是在某些場景下掛起的時間我們是不能接受的(如 web 應用)。

    相應的,併發收集器能夠降低掛起時間,但是也更加複雜,因為在收集的過程中,也會有新的垃圾產生,同時,需要有額外的空間用於在垃圾收集過程中應用程式的繼續使用。

  • 壓縮 vs 不壓縮 vs 複製

    當垃圾收集器標記出記憶體中哪些是活的,哪些是垃圾物件後,收集器可以進行壓縮,將所有活的物件移到一起,這樣新的記憶體分配就可以在剩餘的空間中進行了。經過壓縮後,分配新物件的記憶體空間是非常簡單快速的。

    相對的,不壓縮的收集器只會就地釋放空間,不會移動存活物件。優點就是快速完成垃圾收集,缺點就是潛在的碎片問題。通常,這種情況下,分配物件空間會比較慢比較複雜,比如為新的一個大物件找到合適的空間。

    還有一個選擇就是複製收集器,將活的物件複製到另一塊空間中,優點就是原空間被清空了,這樣後續分配物件空間非常迅速,缺點就是需要進行復制操作和佔用額外的空間。

1  Serial收集器

Serial是最基本、歷史最悠久的垃圾收集器,使用複製演算法,曾經是JDK1.3.1之前新生代唯一的垃圾收集器。

Serial是一個單執行緒的收集器,它不僅僅只會使用一個CPU或一條執行緒去完成垃圾收集工作,並且在進行垃圾收集的同時,必須暫停其他所有的工作執行緒,直到垃圾收集結束。

Serial垃圾收集器雖然在收集垃圾過程中需要暫停所有其他的工作執行緒,但是它簡單高效,對於限定單個CPU環境來說,沒有執行緒互動的開銷,可以獲得最高的單執行緒垃圾收集效率,因此Serial垃圾收集器依然是java虛擬機器執行在Client模式下預設的新生代垃圾收集器。

2  ParNew收集器

ParNew垃圾收集器其實是Serial收集器的多執行緒版本,也使用複製演算法,除了使用多執行緒進行垃圾收集之外,其餘的行為和Serial收集器完全一樣,ParNew垃圾收集器在垃圾收集過程中同樣也要暫停所有其他的工作執行緒。

ParNew收集器預設開啟和CPU數目相同的執行緒數,可以通過-XX:ParallelGCThreads引數來限制垃圾收集器的執行緒數。

ParNew雖然是除了多執行緒外和Serial收集器幾乎完全一樣,但是ParNew垃圾收集器是很多java虛擬機器執行在Server模式下新生代的預設垃圾收集器。

3  Parallel  Scavenge收集器

Parallel Scavenge收集器也是一個新生代垃圾收集器,同樣使用複製演算法,也是一個多執行緒的垃圾收集器,它重點關注的是程式達到一個可控制的吞吐量(Thoughput,CPU用於執行使用者程式碼的時間/CPU總消耗時間,即吞吐量=執行使用者程式碼時間/(執行使用者程式碼時間+垃圾收集時間)),高吞吐量可以最高效率地利用CPU時間,儘快地完成程式的運算任務,主要適用於在後臺運算而不需要太多互動的任務。

Parallel Scavenge收集器提供了兩個引數用於精準控制吞吐量:

a.-XX:MaxGCPauseMillis:控制最大垃圾收集停頓時間,是一個大於0的毫秒數。

b.-XX:GCTimeRation:直接設定吞吐量大小,是一個大於0小於100的整數,也就是程式執行時間佔總時間的比率,預設值是99,即垃圾收集執行最大1%(1/(1+99))的垃圾收集時間。

Parallel Scavenge是吞吐量優先的垃圾收集器,它還提供一個引數:-XX:+UseAdaptiveSizePolicy,這是個開關引數,開啟之後就不需要手動指定新生代大小(-Xmn)、Eden與Survivor區的比例(-XX:SurvivorRation)、新生代晉升年老代物件年齡(-XX:PretenureSizeThreshold)等細節引數,虛擬機器會根據當前系統執行情況收集效能監控資訊,動態調整這些引數以達到最大吞吐量,這種方式稱為GC自適應調節策略,自適應調節策略也是ParallelScavenge收集器與ParNew收集器的一個重要區別。

新生代Parallel Scavenge收集器與ParNew收集器工作原理類似,都是多執行緒的收集器,都使用的是複製演算法,在垃圾收集過程中都需要暫停所有的工作執行緒。

新生代Parallel Scavenge/ParNew與年老代Serial Old搭配垃圾收集過程圖:

4  Serial  Old收集器

Serial Old是Serial垃圾收集器年老代版本,它同樣是個單執行緒的收集器,使用標記-整理演算法,這個收集器也主要是執行在Client預設的java虛擬機器預設的年老代垃圾收集器。

在Server模式下,主要有兩個用途:

a.在JDK1.5之前版本中與新生代的Parallel Scavenge收集器搭配使用。

b.作為年老代中使用CMS收集器的後備垃圾收集方案。

新生代Serial與年老代Serial Old搭配垃圾收集過程圖:

5  Parallel  Old收集器

Parallel Old收集器是Parallel Scavenge的年老代版本,使用多執行緒的標記-整理演算法,在JDK1.6才開始提供。

在JDK1.6之前,新生代使用ParallelScavenge收集器只能搭配年老代的Serial Old收集器,只能保證新生代的吞吐量優先,無法保證整體的吞吐量,Parallel Old正是為了在年老代同樣提供吞吐量優先的垃圾收集器,如果系統對吞吐量要求比較高,可以優先考慮新生代Parallel Scavenge和年老代Parallel Old收集器的搭配策略。

新生代Parallel Scavenge和年老代Parallel Old收集器搭配執行過程圖:

6  CMS收集器

Concurrent mark sweep(CMS)收集器是一種年老代垃圾收集器,其最主要目標是獲取最短垃圾回收停頓時間,和其他年老代使用標記-整理演算法不同,它使用多執行緒的標記-清除演算法。

最短的垃圾收集停頓時間可以為互動比較高的程式提高使用者體驗,CMS收集器是Sun HotSpot虛擬機器中第一款真正意義上併發垃圾收集器,它第一次實現了讓垃圾收集執行緒和使用者執行緒同時工作。

CMS工作機制相比其他的垃圾收集器來說更復雜,整個過程分為以下4個階段:

a.初始標記:只是標記一下GC Roots能直接關聯的物件,速度很快,仍然需要暫停所有的工作執行緒。

b.併發標記:進行GC Roots跟蹤的過程,和使用者執行緒一起工作,不需要暫停工作執行緒。

c.重新標記:為了修正在併發標記期間,因使用者程式繼續執行而導致標記產生變動的那一部分物件的標記記錄,仍然需要暫停所有的工作執行緒。

d.併發清除:清除GC Roots不可達物件,和使用者執行緒一起工作,不需要暫停工作執行緒。

由於耗時最長的併發標記和併發清除過程中,垃圾收集執行緒可以和使用者現在一起併發工作,所以總體上來看CMS收集器的記憶體回收和使用者執行緒是一起併發地執行。

CMS收集器工作過程:

CMS收集器有以下三個不足:

1、CMS收集器對CPU資源非常敏感,其預設啟動的收集執行緒數=(CPU數量+3)/4,在使用者程式本來CPU負荷已經比較高的情況下,如果還要分出CPU資源用來執行垃圾收集器執行緒,會使得CPU負載加重。

2、CMS無法處理浮動垃圾(Floating Garbage),可能會導致Concurrent ModeFailure失敗而導致另一次Full GC。由於CMS收集器和使用者執行緒併發執行,因此在收集過程中不斷有新的垃圾產生,這些垃圾出現在標記過程之後,CMS無法在本次收集中處理掉它們,只好等待下一次GC時再將其清理掉,這些垃圾就稱為浮動垃圾。

CMS垃圾收集器不能像其他垃圾收集器那樣等待年老代機會完全被填滿之後再進行收集,需要預留一部分空間供併發收集時的使用,可以通過引數-XX:CMSInitiatingOccupancyFraction來設定年老代空間達到多少的百分比時觸發CMS進行垃圾收集,預設是68%。

如果在CMS執行期間,預留的記憶體無法滿足程式需要,就會出現一次ConcurrentMode Failure失敗,此時虛擬機器將啟動預備方案,使用Serial Old收集器重新進行年老代垃圾回收。

3、CMS收集器是基於標記-清除演算法,因此不可避免會產生大量不連續的記憶體碎片,如果無法找到一塊足夠大的連續記憶體存放物件時,將會觸發因此Full GC。CMS提供一個開關引數-XX:+UseCMSCompactAtFullCollection,用於指定在Full GC之後進行記憶體整理,記憶體整理會使得垃圾收集停頓時間變長,CMS提供了另外一個引數-XX:CMSFullGCsBeforeCompaction,用於設定在執行多少次不壓縮的Full GC之後,跟著再來一次記憶體整理。

7  G1收集器

Garbage first垃圾收集器是目前垃圾收集器理論發展的最前沿成果,相比與CMS收集器,G1收集器兩個最突出的改進是:

1、基於標記-整理演算法,不產生記憶體碎片;

2、可以非常精確控制停頓時間,在不犧牲吞吐量前提下,實現低停頓垃圾回收。

G1收集器避免全區域垃圾收集,它把堆記憶體劃分為大小固定的多個獨立區域(Region),並且跟蹤這些區域的垃圾收集進度,同時在後臺維護一個優先順序列表,每次根據所允許的收集時間,優先回收垃圾最多的區域。

區域劃分優先順序區域回收機制,確保G1收集器可以在有限時間獲得最高的垃圾收集效率。

G1收集器的運作大致分為下面4個步驟:

1、初始標記:只是標記一下GC Roots能直接關聯到的物件,並且修改TAMS(Next Top at  Mark Start)的值,讓下一階段使用者程式併發執行時,能在正確可用的Region中建立新物件,這階段需要停頓執行緒,但耗時很短;

2、併發標記:從GC Roots開始對堆中物件進行可達性分析,找出存活的物件,這階段耗時較長,但可與使用者程式併發執行;

3、最終標記:為了修正在併發標記期間因使用者程式繼續運作而導致標記產生變動的那一部分標記記錄,虛擬機器將這段時間物件變化記錄線上程Remembered  Set  Logs裡面,最終標記階段需要把Remembered  Set  Logs的資料合併到Remembered  Set中,這階段可停頓執行緒,但可並行處理;

4、篩選回收:首先對各個Region的回收價值和成本進行排序,根據使用者所期望的GC停頓時間來制定回收計劃。這階段將停頓使用者執行緒以提高收集效率。

Java虛擬機器常用的垃圾收集器相關引數如下:

java虛擬機器的-XX:+PrintGCDetails引數可以列印垃圾收集器的日誌資訊。

-verbose:gc可以檢視Java虛擬機器垃圾收集結果。

8  需要注意的幾點

8.1  物件優先在Eden分配

大多數情況下,物件在新生代Eden區中分配。當Eden區中沒有足夠的空間進行分配時,虛擬機器將發起一次GC。

新生代GC與老年代GC:

1、新生代GC(Minor  GC):指發生在新生代的垃圾收集動作,因為Java物件大多都是朝生夕滅的特性,所以Minor  GC非常頻繁,一般回收速度也比較快;

2、老年代GC(Major  GC / Full  GC):指發生在老年代的GC,出現了Major GC,經常會伴隨至少一次的Minor  GC(但並非絕對的,在Parallel  Scavenge收集器的收集策略裡就有直接進行Major  GC的策略選擇過程)。Major  GC的速度一般會比Minor  GC慢10倍以上。

8.2  大物件直接進入老年代

所謂的大物件是指,需要大量連續記憶體空間的Java物件,最典型的大物件就是那種很長的字串以及陣列。

經常出現大物件導致記憶體還有不少空間時就提前觸發垃圾收集以獲取足夠的連續空間來”安置“它們。為了避免在Eden區及兩個Survivor區之間發生大量的記憶體複製,虛擬機器提供了一個-XX:PretenureSizeThreshold引數,令大於這個設定值的物件直接在老年代中分配。

8.3  長期存活的物件將進入老年代

既然虛擬機器採用了分代收集的思想來管理記憶體,那麼記憶體回收時就必須能識別哪些物件應該放在新生代,哪些物件應該放到老年代中。為了做到這點,虛擬機器給每個物件定義了一個物件年齡(Age)計數器。如果物件在Eden區出生並經過第一次Minor  GC後仍然存活,並且能被Survivor容納的話,將移動到Survivor空間中,並且物件年齡設為1.物件在Survivor區中每”熬過“一次Minor  GC,年齡就增加1歲,當它的年齡增加到一定程度(預設15歲),就會被晉升到老年代中。物件晉升老年代的年齡閾值,可以通過引數-XX:MaxTenuringThreshold設定。

8.4  動態物件年齡判斷

為了更好地適應不同程式的記憶體狀況,虛擬機器並不是永遠地要求物件的年齡必須達到了MaxTenuringThreshold才能晉升到老年代,如果在Survivor空間中相同年齡所有物件大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的物件就可以直接進入老年代,無需等到MaxTenuringThreshold中要求的年齡。

8.5  空間分配擔保

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




G1垃圾收集器介紹

轉載自:G1垃圾收集器介紹:https://blog.csdn.net/a724888/article/details/78764006

G1 的主要關注點在於達到可控的停頓時間,在這個基礎上儘可能提高吞吐量,這一點非常重要。

G1 被設計用來長期取代 CMS 收集器,和 CMS 相同的地方在於,它們都屬於併發收集器,在大部分的收集階段都不需要掛起應用程式。區別在於,G1 沒有 CMS 的碎片化問題(或者說不那麼嚴重),同時提供了更加可控的停頓時間。

如果你的應用使用了較大的堆(如 6GB 及以上)而且還要求有較低的垃圾收集停頓時間(如 0.5 秒),那麼 G1 是你絕佳的選擇,是時候放棄 CMS 了。

閱讀建議:本文力求用簡單的話介紹清楚 G1 收集器,但是並不會重複介紹每一個細節,所以希望讀者瞭解其他幾個收集器的工作過程,尤其是 CMS 收集器。

G1 總覽

首先是記憶體劃分上,之前介紹的分代收集器將整個堆分為年輕代、老年代和永久代,每個代的空間是確定的。

而 G1 將整個堆劃分為一個個大小相等的小塊(每一塊稱為一個 region),每一塊的記憶體是連續的。和分代演算法一樣,G1 中每個塊也會充當 Eden、Survivor、Old 三種角色,但是它們不是固定的,這使得記憶體使用更加地靈活。

1

執行垃圾收集時,和 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 的收集過程,G1 收集器主要包括了以下 4 種操作:

  • 1、年輕代收集
  • 2、併發收集,和應用執行緒同時執行
  • 3、混合式垃圾收集
  • *、必要時的 Full GC

接下來,我們進行一一介紹。

年輕代收集

首先,我們來看下 G1 的堆結構:

3

年輕代中的垃圾收集流程(Young GC):

4

我們可以看到,年輕代收集概念上和之前介紹的其他分代收集器大差不差的,但是它的年輕代會動態調整。

Old 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) 演算法。

    Oracel 的資料顯示,這個階段會回收完全空閒的區塊

  5. 清理:清理階段真正回收的記憶體很少。

到這裡,G1 的一個併發週期就算結束了,其實就是主要完成了垃圾定位的工作,定位出了哪些分割槽是垃圾最多的。

混合垃圾回收週期

併發週期結束後是混合垃圾回收週期,不僅進行年輕代垃圾收集,而且回收之前標記出來的老年代的垃圾最多的部分割槽塊。

混合垃圾回收週期會持續進行,直到幾乎所有的被標記出來的分割槽(垃圾佔比大的分割槽)都得到回收,然後恢復到常規的年輕代垃圾收集,最終再次啟動併發週期。

Full GC

到這裡我們已經說了年輕代收集、併發週期、混合回收週期了,大家要熟悉這幾個階段的工作。

下面我們來介紹特殊情況,那就是會導致 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 引數配置和最佳實踐

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 的老年代

    不要設定年輕代為固定大小,否則:

    • 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,這個我們通常指定整堆大小就好了。




各種GC的觸發時機(When)

GC型別

說到GC型別,就更有意思了,為什麼呢,因為業界沒有統一的嚴格意義上的界限,也沒有嚴格意義上的GC型別,都是左邊一個教授一套名字,右邊一個作者一套名字。為什麼會有這個情況呢,因為GC型別是和收集器有關的,不同的收集器會有自己獨特的一些收集型別。所以作者在這裡引用R大關於GC型別的介紹,作者覺得還是比較妥當準確的。如下:

  • Partial GC:並不收集整個GC堆的模式
    • Young GC(Minor GC):只收集young gen的GC
    • Old GC:只收集old gen的GC。只有CMS的concurrent collection是這個模式
    • Mixed GC:收集整個young gen以及部分old gen的GC。只有G1有這個模式
  • Full GC(Major GC):收集整個堆,包括young gen、old gen、perm gen(如果存在的話)等所有部分的模式。

觸發時機

上面大家也看到了,GC型別分分類是和收集器有關的,那麼當然了,對於不同的收集器,GC觸發時機也是不一樣的,作者就針對預設的serial GC來說:

  • young GC:當young gen中的eden區分配滿的時候觸發。注意young GC中有部分存活物件會晉升到old gen,所以young GC後old gen的佔用量通常會有所升高。
  • full GC:當準備要觸發一次young GC時,如果發現統計資料說之前young GC的平均晉升大小比目前old gen剩餘的空間大,則不會觸發young GC而是轉為觸發full GC(因為HotSpot VM的GC裡,除了CMS的concurrent collection之外,其它能收集old gen的GC都會同時收集整個GC堆,包括young gen,所以不需要事先觸發一次單獨的young GC);或者,如果有perm gen的話,要在perm gen分配空間但已經沒有足夠空間時,也要觸發一次full GC;或者System.gc()、heap dump帶GC,預設也是觸發full GC。

FULL GC觸發條件詳解

除直接呼叫System.gc外,觸發Full GC執行的情況有如下四種。

1. 舊生代空間不足

舊生代空間只有在新生代物件轉入及建立為大物件、大陣列時才會出現不足的現象,當執行Full GC後空間仍然不足,則丟擲如下錯誤:

java.lang.OutOfMemoryError: Java heap space 

為避免以上兩種狀況引起的Full GC,調優時應儘量做到讓物件在Minor GC階段被回收、讓物件在新生代多存活一段時間及不要建立過大的物件及陣列。

2. Permanet Generation空間滿

Permanet Generation中存放的為一些class的資訊等,當系統中要載入的類、反射的類和呼叫的方法較多時,Permanet Generation可能會被佔滿,在未配置為採用CMS GC的情況下會執行Full GC。如果經過Full GC仍然回收不了,那麼JVM會丟擲如下錯誤資訊:

java.lang.OutOfMemoryError: PermGen space 

為避免Perm Gen佔滿造成Full GC現象,可採用的方法為增大Perm Gen空間或轉為使用CMS GC。

3. CMS GC時出現promotion failed和concurrent mode failure

對於採用CMS進行舊生代GC的程式而言,尤其要注意GC日誌中是否有promotion failed和concurrent mode failure兩種狀況,當這兩種狀況出現時可能會觸發Full GC。

promotion failed是在進行Minor GC時,survivor space放不下、物件只能放入舊生代,而此時舊生代也放不下造成的;concurrent mode failure是在執行CMS GC的過程中同時有物件要放入舊生代,而此時舊生代空間不足造成的。

應對措施為:增大survivor space、舊生代空間或調低觸發併發GC的比率,但在JDK 5.0+、6.0+的版本中有可能會由於JDK的bug29導致CMS在remark完畢後很久才觸發sweeping動作。對於這種狀況,可通過設定-XX: CMSMaxAbortablePrecleanTime=5(單位為ms)來避免。

4. 統計得到的Minor GC晉升到舊生代的平均大小大於舊生代的剩餘空間

這是一個較為複雜的觸發情況,Hotspot為了避免由於新生代物件晉升到舊生代導致舊生代空間不足的現象,在進行Minor GC時,做了一個判斷,如果之前統計所得到的Minor GC晉升到舊生代的平均大小大於舊生代的剩餘空間,那麼就直接觸發Full GC。

例如程式第一次觸發Minor GC後,有6MB的物件晉升到舊生代,那麼當下一次Minor GC發生時,首先檢查舊生代的剩餘空間是否大於6MB,如果小於6MB,則執行Full GC。

當新生代採用PS GC時,方式稍有不同,PS GC是在Minor GC後也會檢查,例如上面的例子中第一次Minor GC後,PS GC會檢查此時舊生代的剩餘空間是否大於6MB,如小於,則觸發對舊生代的回收。

除了以上4種狀況外,對於使用RMI來進行RPC或管理的Sun JDK應用而言,預設情況下會一小時執行一次Full GC。可通過在啟動時通過- java -Dsun.rmi.dgc.client.gcInterval=3600000來設定Full GC執行的間隔時間或通過-XX:+ DisableExplicitGC來禁止RMI呼叫System.gc。

總結一下就是:

 Minor GC ,Full GC 觸發條件

Minor GC觸發條件:當Eden區滿時,觸發Minor GC。

Full GC觸發條件:

(1)呼叫System.gc時,系統建議執行Full GC,但是不必然執行

(2)老年代空間不足

(3)方法去空間不足

(4)通過Minor GC後進入老年代的平均大小大於老年代的可用記憶體

(5)由Eden區、From Space區向To Space區複製時,物件大小大於To Space可用記憶體,則把該物件轉存到老年代,且老年代的可用記憶體小於該物件大小


參考及推薦:

1、JVM垃圾回收基本原理和演算法:https://blog.csdn.net/a724888/article/details/77981592

2、垃圾回收器詳解:https://blog.csdn.net/a724888/article/details/78764006