之前看過《深入瞭解Java虛擬機》感覺容易忘,今天寫一篇博客加深一下印象。 JVM的記憶體分配和垃圾回收(GC)主要發生在Java堆中。而Java堆根據對象的存活時間可以分為新生代和老年代,而新生代又細分為Eden區、From Survivor區、To Survivor區,這是由於新生代中的垃圾回收算 ...
之前看過《深入瞭解Java虛擬機》感覺容易忘,今天寫一篇博客加深一下印象。
JVM的記憶體分配和垃圾回收(GC)主要發生在Java堆中。而Java堆根據對象的存活時間可以分為新生代和老年代,而新生代又細分為Eden區、From Survivor區、To Survivor區,這是由於新生代中的垃圾回收演算法基本都是複製演算法。
1.對象優先在Eden區中分配
當Eden區沒有足夠空間進行分配時,虛擬機會發起一次新生代GC(Minor GC)。因為Java對象大多數都具有朝生夕滅的特性,所以Minor GC非常頻繁,回收速度也比較快。
我們可以通過參數-XX:+PrintGCDetails來查看GC日誌。下麵舉個實際例子來看看是不是優先在Eden區中分配記憶體。
-verbose:gc
-Xms20M
-Xmx20M
-Xmn10M
-XX:+PrintGCDetails
-XX:SurvivorRatio=8
設置的參數
package MinorGC;
public class MinorGC {
private static final int _1MB = 1024*1024;
public static void main(String[] args) {
byte[] allocation1,allocation2,allocation3;
allocation1 = new byte[1*_1MB];
allocation2 = new byte[2*_1MB];
allocation3 = new byte[2*_1MB];
}
}
可以看到控制台輸出
新生代的記憶體使用情況:total 9216K, used 7482K
老年代的記憶體使用情況:total 10240K, used 0K
這樣可以看出對象優先在Eden區中分配。
2.大對象直接進入老年代
大對象是指需要大量連續記憶體空間的Java對象,例如很長的字元串和數組,上面代碼中的byte[]數組就是大數組。經常出現大對象容易導致記憶體中還有不少記憶體就要提前GC來獲取連續的空間來安放它們。
虛擬機中可以設置參數-XX:PretenureSizeThreshold參數來讓大於這個設置值的對象直接在老年代分配,這樣的目的是避免在Eden區和兩個Survivor區之間進行大量的記憶體複製。
下麵測試一下,這時候要註意一下我用的是Java8,預設的垃圾回收器是Parallel Scavenge(新生代)+Parallel Old(老年代),而Parallel Scavenge不認識參數
-XX:PretenureSizeThreshold(Parallel Scavenge不需要設置),所以要先加上-XX:+UseSerialGC來將虛擬機的垃圾回收器設置成Serial / Serial Old回收器。
-verbose:gc
-Xms20M
-Xmx20M
-Xmn10M
-XX:+PrintGCDetails
-XX:SurvivorRatio=8
-XX:PretenureSizeThreshold=3145728
-XX:+UseSerialGC
設置的參數
package MinorGC;
public class MinorGC {
private static final int _1MB = 1024*1024;
public static void main(String[] args) {
byte[] allocation4;
// allocation1 = new byte[_1MB];
// allocation2 = new byte[2*_1MB];
// allocation3 = new byte[2*_1MB];
allocation4 = new byte[4*_1MB];
}
}
可以發現4MB的對象直接放到老年代中了
3.長期存活的對象將進入老年區
虛擬機給每個對象定義一個對象年齡計數器。如果對象在Eden區出生並經過第一次Minor GC後仍然存活,並能被Survivor容納,將被移動到Survivor的空間中,並且對象年齡設為1對象在Survivor區每“熬過”一次Minor GC,年齡就增加1歲,當它的年齡增加到一定程度(預設15歲),就將被晉升到老年代中。JVM中可以通過-XX:MaxTenuringThreshold設置。
下麵我們測試一下
-verbose:gc
-Xms20M
-Xmx20M
-Xmn10M
-XX:+PrintGCDetails
-XX:SurvivorRatio=8
-XX:MaxTenuringThreshold=1
-XX:+PrintTenuringDistribution
-XX:+UseSerialGC
設置的參數
package MinorGC;
public class MinorGC {
private static final int _1MB = 1024*1024;
@SuppressWarnings("unused")
public static void main(String[] args) {
byte[] allocation1, allocation2, allocation3;
allocation1 = new byte[_1MB / 4];
// 什麼時候進入老年代取決於XX:MaxTenuringThreshold設置
allocation2 = new byte[4 * _1MB];
allocation3 = new byte[4 * _1MB];
allocation3 = null;
allocation3 = new byte[4 * _1MB];
}
}
該方法中allocation1對象需要256KB記憶體,Survivor區可以容納,所以講它放到Survivor區中並讓它年齡加1,。在第二次GC發生後它就進入老年代了,而Survivor區剛好被清除乾凈。
4.動態對象年齡判定
為了更好地適應不同程式的記憶體狀況,虛擬機並不是永遠要求對象的年齡必須達到了MaxTenuringThreshold才晉升老年代,如果在Survivor空間中相同年齡所有對象大小的總和大於Survivor空間的一半, 年齡大於或等於該年齡的對象就可以直接進入老年代,無須等到MaxTenuringThreshold中要求的年齡。
測試
配置和前面的一樣,只是MaxTenuringThreshold=15
package MinorGC;
public class MinorGC {
private static final int _1MB = 1024*1024;
@SuppressWarnings("unused")
public static void main(String[] args) {
byte[] allocation1, allocation2, allocation3, allocation4;
allocation1 = new byte[_1MB / 4];
// allocation1+allocation2大於survivo空間一半
allocation2 = new byte[_1MB / 4];
allocation3 = new byte[4 * _1MB];
allocation4 = new byte[4 * _1MB];
allocation4 = null;
allocation4 = new byte[4 * _1MB];
}
}
發現運行結果中Survivor的空間占用仍然為0%,而老年代比預期增加了6%,也就是說,allocation1、allocation2對象都直接進入了老年代,而沒有等到15歲的臨界年齡。因為這兩個對象加起來已經到達了
512KB,並且它們是同年的,滿足同年對象達到Survivor空間的一半規則。
在註釋掉allocation2後,發現只有一個256KB進入了老年代。
5.空間分配擔保
在發生Minor GC之前,虛擬機會先檢查老年代最大可用的連續空間是否大於新生代所有對象總空間,如果這個條件成立,那麼Minor GC可以確保是安全的。如果不成立,則虛擬機會查看HandlePromotionFailure設置值是否允許擔保失敗。如果允許,那麼會繼續檢查老年代最大可用的連續空間是否大於歷次晉升到老年代對象的平均大小,如果大於,將嘗試著進行一次Minor GC,儘管這次Minor GC是有風險的;如果小於,或者HandlePromotionFailure設置不允許冒險,那這時也要改為進行一次Full GC。
新生代使用複製收集演算法,但為了記憶體利用率,只使用其中一個Survivor空間來作為輪換備份,因此當出現大量對象在Minor GC後仍然存活的情況(最極端的情況就是記憶體回收後新生代中所有對象都存活),就需要老年代進行分配擔保,把Survivor無法容納的對象直接進入老年代。與生活中的貸款擔保類似,老年代要進行這樣的擔保,前提是老年代本身還有容納這些對象的剩餘空間,一共有多少對象會活下來在實際完成記憶體回收之前是無法明確知道的,所以只好取之前每一次回收晉升到老年代對象容量的平均大小值作為經驗值,與老年代的剩餘空間進行比較,決定是否進行Full GC來讓老年代騰出更多空間。
取平均值進行比較其實仍然是一種動態概率的手段,也就是說,如果某次Minor GC存活後的對象突增,遠遠高於平均值的話,依然會導致擔保失敗(Handle Promotion Failure)。如果出現了HandlePromotionFailure失敗,那就只好在失敗後重新發起一次Full GC。雖然擔保失敗時繞的圈子是最大的,但大部分情況下都還是會將HandlePromotionFailure開關打開,避免Full GC過於頻繁。
補充一下參數有哪些:
1、-Xmx –Xms:指定最大堆和最小堆
2、-Xmn、-XX:NewRatio、-XX:SurvivorRatio:
- -Xmn:設置新生代大小
- -XX:NewRatio:新生代(eden+2*s)和老年代(不包含永久區)的比值
例如:4,表示新生代:老年代=1:4,即新生代占整個堆的1/5
- -XX:SurvivorRatio(幸存代)設置兩個Survivor區和eden的比值
例如:8,表示兩個Survivor:eden=2:8,即一個Survivor占年輕代的1/10
參考自:深入理解JVM
https://www.jianshu.com/p/fa3569127416