深入理解Java虛擬機器(四)Eden、Survivor、老年代、GC日誌
1. 年輕代
1.1 Eden區和Survivor區
新生代GC(Minor GC):指發生在新生代的垃圾收集動作,Minor GC非常頻繁,新生代採用複製演算法,一般回收速度也比較快。因為採用複製演算法,所以年輕代分為三部分:1個Eden區和2個Survivor區(分別叫From和To),預設比例為8:1。GC的流程如下:
- 在GC開始時,物件只存在於Eden區和From區,To是空的。
- 緊接著,Eden區中所有存活的物件都會被複制到To,而在From區中,仍存活的物件會根據他們的年齡值來決定去向。
- 年齡達到一定值(年齡值可以通過
-XX:MaxTenuringThreshold
- 沒有達到閾值的物件會被複制到To區。
- 這次GC後,Eden區和From區已經被清空。
- 此時From和To會交換他們的角色。也就是新的To就是上次GC前的From,新的From就是上次GC前的To。不管怎樣,都會保證名為To的Survivor區域是空的。
- Minor GC會一直重複這樣的過程,直到To區被填滿,To區被填滿之後,會將所有物件移動到年老代中。
如果在Survivor空間中相同年齡所有物件大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的物件就可以直接進入老年代,無須等到MaxTenuringThreshold中要求的年齡。
1.2 有關年輕代的JVM引數
-
-XX:NewSize
和-XX:MaxNewSize
:用於設定年輕代的大小,建議設為整個堆大小的1/3或者1/4,兩個值設為一樣大。 -
-XX:SurvivorRatio
:用於設定Eden和其中一個Survivor的比值。 -
-XX:+PrintTenuringDistribution
:這個引數用於顯示每次Minor GC時Survivor區中各個年齡段的物件的大小。 -
-XX:InitialTenuringThreshol
和-XX:MaxTenuringThreshold
:用於設定晉升到老年代的物件年齡的最小值和最大值。
2. 年老代。
老年代GC(Major GC/Full GC):指發生在老年代的GC,出現了Major GC,經常會伴隨至少一次的Minor GC(但非絕對的,在Parallel Scavenge收集器的收集策略裡就有直接進行Major GC的策略選擇過程)。Major GC的速度一般會比Minor GC慢10倍以上。
大物件直接進入老年代。所謂的大物件是指,需要大量連續記憶體空間的Java物件,最典型的大物件就是那種很長的字串以及陣列(byte[]陣列就是典型的大物件)。大物件對虛擬機器的記憶體分配來說就是一個壞訊息(替Java虛擬機器抱怨一句,比遇到一個大物件更加壞的訊息就是遇到一群“朝生夕滅”的“短命大物件”,寫程式的時候應當避免),經常出現大物件容易導致記憶體還有不少空間時就提前觸發垃圾收集以獲取足夠的連續空間來“安置”它們。
長期存活的物件將進入老年代。虛擬機器給每個物件定義了一個物件年齡(Age)計數器。如果物件在Eden出生並經過第一次Minor GC後仍然存活,並且能被Survivor容納的話,將被移動到Survivor空間中,並且物件年齡設為1。物件在Survivor區中每“熬過”一次Minor GC,年齡就增加1歲,當它的年齡增加到一定程度(預設為15歲),就將會被晉升到老年代中。
2.1 年老代涉及的引數
-XX:PretenureSizeThreshold
令大於這個設定值的物件直接在老年代分配。這樣做的目的是避免在Eden區及兩個Survivor區之間發生大量的記憶體複製(新生代採用複製演算法收集記憶體)。
2.2 空間分配擔保
在發生Minor GC之前,虛擬機器會先檢查老年代最大可用的連續空間是否大於新生代所有物件總空間,如果這個條件成立,那麼Minor GC可以確保是安全的。如果不成立,則虛擬機器會檢視HandlePromotionFailure
設定值是否允許擔保失敗。如果允許,那麼會繼續檢查老年代最大可用的連續空間是否大於歷次晉升到老年代物件的平均大小,如果大於,將嘗試著進行一次Minor GC,儘管這次Minor GC是有風險的;如果小於,或者HandlePromotionFailure
設定不允許冒險,那這時也要改為進行一次Full GC。
下面解釋一下“冒險”是冒了什麼風險,前面提到過,新生代使用複製收集演算法,但為了記憶體利用率,只使用其中一個Survivor空間來作為輪換備份,因此當出現大量物件在MinorGC後仍然存活的情況(最極端的情況就是記憶體回收後新生代中所有物件都存活),就需要老年代進行分配擔保,把Survivor無法容納的物件直接進入老年代。與生活中的貸款擔保類似,老年代要進行這樣的擔保,前提是老年代本身還有容納這些物件的剩餘空間,一共有多少物件會活下來在實際完成記憶體回收之前是無法明確知道的,所以只好取之前每一次回收晉升到老年代物件容量的平均大小值作為經驗值,與老年代的剩餘空間進行比較,決定是否進行Full GC來讓老年代騰出更多空間。
取平均值進行比較其實仍然是一種動態概率的手段,也就是說,如果某次Minor GC存活後的物件突增,遠遠高於平均值的話,依然會導致擔保失敗(Handle Promotion Failure)。如果出現了HandlePromotionFailure失敗,那就只好在失敗後重新發起一次Full GC。雖然擔保失敗時繞的圈子是最大的,但大部分情況下都還是會將HandlePromotionFailure開關開啟,避免Full GC過於頻繁。
3. 日誌示例:
虛擬機器提供了-XX:+PrintGCDetails
這個收集器日誌引數,告訴虛擬機器在發生垃圾收集行為時列印記憶體回收日誌,並且在程序退出的時候輸出當前的記憶體各區域分配情況。
33.125:[GC[DefNew:3324K->152K(3712K),0.0025925 secs]3324K->152K(11904K),0.0031680 secs]
100.667[FullGC[Tenured:0K->210K(10240K),0.0149142 secs]4603K->210K(19456K),[Perm:2999K->2999K(21248K)],0.0150007 secs][Times:user=0.01 sys=0.00,real=0.02 secs]
- 最前面的數字
33.125:
和100.667:
代表了GC發生的時間。 - GC日誌開頭的
[GC
和[Full GC
說明了這次垃圾收集的停頓型別,而不是用來區分新生代GC還是老年代GC的。 - 如果有
Full
,說明這次GC是發生了Stop-The-World的。 - 方括號內部的3324K->152K(3712K)表示
GC前該記憶體區域已使用容量->GC後該記憶體區域已使用容量(該記憶體區域總容量)
。 - 方括號外部的3324K->152K(11904K)表示
GC前Java堆已使用容量->GC後Java堆已使用容量(Java堆總容量)
。 0.0025925 secs
表示該記憶體區域GC所佔用的時間,單位是秒。[Times:user=0.01 sys=0.00,real=0.02 secs]
,這裡面的user、sys和real與Linux的time命令所輸出的時間含義一致,分別代表使用者態消耗的CPU時間、核心態消耗的CPU事件和操作從開始到結束所經過的牆鍾時間(Wall Clock Time)。- CPU時間與牆鍾時間的區別是,牆鍾時間包括各種非運算的等待耗時,例如等待磁碟I/O、等待執行緒阻塞,而CPU時間不包括這些耗時,但當系統有多CPU或者多核的話,多執行緒操作會疊加這些CPU時間,所以讀者看到user或sys時間超過real時間是完全正常的。