大家好,我們今天給大家介紹,Java虛拟機優先在Eden區建立對象。我們通過以下參數:-XX:+UseParallelGC -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails,設定一種實驗場景:
Java堆大小為20MB,并且是不可擴充,其中10M分給新生代,剩下的10MB分給老年代。參數-XX:SurvivorRatio=8決定了新生代的Eden區和S0、S1存活區的空間比例是8:1:1。相等于:Eden區的大小是8M,兩個Survivor區的大小都是1M。
Java虛拟機通過-XX:+PrintGCDetails日志參數列印日志。
圖1-1 Java堆的分代垃圾回收政策
我們也針對這篇文章錄制了一期視訊:動畫+源碼,驗證Java虛拟機優先在Eden區建立對象
圖1-1的左側是建立Java對象的流程圖,右側是在Java堆的分代垃圾管理政策。我們一起來看Java虛拟機如何優先在Eden區中建立對象。
一 對象建立過程
① 我們先建立一個大小為2M的紅色對象,需要判斷Eden區是否有足夠的空間,此時Eden區有8M的空閑空間,按照優先在Eden區建立對象的原則,成功的在Eden區建立一個大小為2M的紅色對象,如圖1-2所示:
圖1-2 在Eden區建立一個2M的Java對象
② 我們再建立一個大小為2M的綠色對象,此時Eden區有6M的空間,該對象建立成功。同理,再建立一個2M大小的藍色對象,仍然可以建立成功。如圖1-3所示:
圖1-3 再次向Java堆中建立兩個2M的Java對象
③ 我們現在要建立一個大小為4M的紅色對象,此時Eden區隻有2M的空間,不能存儲4M的對象,需要發起一次Minor GC。如圖1-4所示:
圖1-4 發起一次Minor GC
④ 标記存活對象:需要标記Eden區的存活對象,如果發現這三個對象都是存活對象。是以,需要把它們複制到一個存活區(S0或者S1)中。但是每個存活區的空間隻有1M,容不下任何一個存活對象。
⑤ 将存活對象晉升到老年代:是以,按照這個建立對象的流程圖,就需要把這三個對象依次拷貝到老年代。然後清除掉Eden區中的所有對象。如圖1-5所示:
圖1-5 晉升到老年代
⑥ 在Eden區建立4M大小的新對象:此時,Eden區有8M的剩餘空間,足以放得下4M的紅色對象。然後将新對象建立到Eden區,如圖1-6所示:
圖1-6 在Eden區建立新對象
二 對象建立過程總結
圖2-1 建立Java對象的流程圖
- 建立對象的流程如圖2-1所示,建立新對象,首先判斷Eden區是否有足夠的空間存放該對象;如果Eden區空間充足,直接在Eden區建立對象;否則,需要觸發一次Minor GC,然後需要判斷Servivor存活區是否放得下Eden區的存活對象。
- 如果S0或者S1有足夠的存儲空間,就把Eden區的存活對象拷貝到S0或者S1區中。每次發生Minor GC,存活對象的年齡+1,如果存活對象的年齡<15歲,就會不斷的在Eden區來回複制;否則,當存活對象的年齡=15歲時,就将存活對象拷貝到老年代。
- 在我們這個示例中,因為Servivor隻有1M,不能存儲任何一個存活對象。就需要把存活對象拷貝到老年代。
- 然後清除Eden區的所有對象,如果此時Eden區有足夠的空間,就把新對象建立到Eden區。否則,說明現在建立的是一個大對象,就把新對象建立到老年代,預設情況下老年代的空間是年輕代空間的兩倍,通常情況下有足夠的空間。
- 如果老年代的空間不足,就需要觸發Full GC,清除老年代的垃圾對象,讓老年代能騰挪出更多的空間。
- 發生Full GC之後,需要再次判斷老年代,是否有足夠的空間存放新對象。
- 如果有足夠的空間,就配置設定記憶體,否則就隻有報OOM,記憶體溢出了。
三 代碼驗證優先在Eden區建立對象
我們現在用Java代碼的方式,驗證Java虛拟機優先在Eden區建立對象。通過-verbose:gc -XX:+UseSerialGC -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails參數,設定Java堆大小為20MB,新生代10M,兩個Survivor各為1M,Eden區8M。
public class PriorityAllocateEden {
public static void main(String[] args) {
testAllocation();
}
/**
* -verbose:gc -XX:+UseSerialGC -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails
* -verbose:gc 表示輸出虛拟機中GC的詳細情況
* -XX:+PrintGCDetails 列印GC細節
* -XX:+UseSerialGC 選擇了Serial(新生代) + SeialOld(年老代)垃圾收集器組合
* -Xms 設定程式啟動時占用記憶體大小
* -Xmx 設定程式運作期間最大可占用的記憶體大小
* -Xmn 是指年輕代的大小
* -XX:SurvivorRatio=8 是指Eden : Survivor大小為 8:1
*/
public static void testAllocation(){
byte alloc1[],alloc2[],alloc3[],alloc4[];
alloc1=new byte[2 * 1024*1024]; // 2M
alloc2=new byte[2 * 1024*1024]; // 2M
alloc3=new byte[2 * 1024*1024]; // 2M
alloc4=new byte[4 * 1024*1024]; // 4M 觸發一次 Minor GC
}
}
通過java -verbose:gc -XX:+UseSerialGC -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails PriorityAllocateEden指令運作Java代碼。
gc日志
我們先解讀第一行日志:
[GC (Allocation Failure) [DefNew: 6651K->257K(9216K), 0.0691040 secs] 6651K->6401K(19456K), 0.0691840 secs] [Times: user=0.00 sys=0.07, real=0.07 secs] Heap
- GC:表明發生了一次垃圾回收,前面沒有Full修飾,表明這是一次Minor GC。
- Allocation Failure:觸發本次GC的原因是,年輕代的Eden區沒有足夠的空間存儲新對象。
- DefNew:使用的是UseSerialGC,單線程垃圾收集器。
- 6651K->257K(9216K):這三個數的含義是,GC前年輕代使用的容量(6M),GC後年輕代使用的容量(257K),年輕代的總可用容量(9M),為什麼不是10M,因為年輕代的可用空間=1個Survivor+Eden區=9M,另一個Survivor為不可用區(專門用來當粘貼闆了)。
- 0.0691040 secs:年輕代的Minor GC耗時,機關秒。
- 6651K->6401K(19456K):整個堆回收前的大小,整個堆回收後的大小(幾乎未回收到什麼記憶體),堆的總大小。
- Times: user=0.00 sys=0.07, real=0.07 secs:分别表示使用者态耗時,核心态耗時和實際耗時。
四 分析GC日志得出以下結論
在執行alloc4=new byte[4 * 1024 * 1024];時,發生了一次Minor GC,這次回收的結果是年輕代由6651K變為257K,而總記憶體的使用量(6651K->6401K)幾乎沒有減少,說明alloc1、alloc2和alloc3都是存活對象,虛拟機幾乎沒有找到可回收的對象。
發生這次垃圾回收的原因是,為alloc4配置設定記憶體時,發現Eden區已經使用了(6MB+257KB),剩餘空間不足以配置設定給alloc4所需的4MB記憶體,是以發生Minor GC。
在标記-清除階段又發現,存活的三個2MB大小的對象都無法copy到Survivor空間(Survivor空間隻有1MB),是以隻好通過配置設定擔保機制将這些對象copy到老年代。
這次垃圾回收之後,4MB的alloc4對象順利的配置設定到了Eden區,alloc1、alloc2、alloc3進入到了老年代。
通過GC日志也驗證了,Java虛拟機優先在Eden區建立對象,為什麼要優先在Eden區建立對象呢?因為Eden區垃圾回收的效率比老年代要高很多。