Java 技術體系中的記憶體管理解決了兩個問題 : 自動給物件分配記憶體與回收物件的記憶體. 關於回收記憶體這一點可以參考 Part1 與 Part2. 這邊要來探討物件分配給記憶體的觀念.
物件的記憶體分配簡單來說就是在 Heap 上進行分配, 而分配的主要對象會是新生代的 Eden 區上. 而分配的規則並不是百分百固定, 其細節取決於當前使用的是哪一種垃圾收集器, 還有 JVM 中的記憶體相關的參數設置. 接下來我們會講解幾條普遍的記憶體分配規則, 並透過程式碼來驗證這些規則. 這邊程式碼程式的測試是使用 Client 模式 JVM 執行環境 ; 而驗證使用的垃圾收集組合是 Serial/Serial Old.
對象優先在 Eden 分配 :
大多數情況下, 物件在新生代 Eden 區中分配. 當 Eden 區沒有空間進行分配時, JVM 將發起一次 Minor GC.
JVM 提供了 -XX:+PrintGCDetails 參數可以顯示收集器參數日誌, 讓 JVM 發生垃圾收集行為時列印記憶體回收日誌, 並在進程退出的時候輸出當前記憶體各區域的分配情況. 在實際應用中, 記憶體回收日誌一般是列印到檔案後透過日誌工具進行分析.
下面程式代碼中嘗試分配 3 個 2MB 大小和一個 4MB 大小的對象, 在執行時期透過 -Xmx20M, -Xms20M 和 -Xmn10M 這三個參數限制 Java Heap 大小為 20 MB 且不可延伸, 其中 10MB 分配給新生代, 剩下 10MB 分配給老年代. -XX:SurvivorRatio=8 決定新生代中 Eden 區與一個 Survivor 區的空間比例是 8 比 1.
執行 main() 方法中在分配 4MB 記憶體時會發生一次 Minor GC, 這次發生原因是因為在分配記憶體時, Eden 已經被占用 6MB, 剩餘空間已不足分配 4MB 記憶體, 因此發生 Minor GC. GC 期間 JVM 又發現已有的 3 個 2MB 大小的物件全部無法放入 Survivor 空間 (Survivor 空間只有 1MB 大小), 所以只好透過分配擔保機制提前轉移到老年代去. 這次 GC 結束後, 4MB 的記憶體將被分配到 Eden 中, 而原先的 6MB 將回被移到老年代去 :
- Ex3_3.java :
- package ch03;
- public class Ex3_3 {
- public static int _1MB = 1024*1024;
- /**
- * Goal : Test GC 新生代 Minor GC
- * VM Args : -verbose:gc -XX:+UseSerialGC -XX:+PrintGCDetails -Xms20M -Xmx20M -Xmn10m -XX:SurvivorRatio=8
- * @param args
- */
- public static void main(String[] args) throws Exception{
- byte[] alloc1=null, alloc2=null, alloc3=null, alloc4=null;
- byte[][] allocMtx = {alloc1, alloc2, alloc3, alloc4};
- for(int i=1; i<=3 ; i++)
- {
- System.out.printf("\t[Info] Allocate 2M memory for alloc%d...\n", i);
- allocMtx[i-1] = new byte[2 * _1MB];
- }
- //Thread.sleep(1000);
- System.out.printf("\t[Info] Allocate 4M memory for alloc4..\n");
- allocMtx[3] = new byte[4 * _1MB]; // 出現一次 Minor GC
- }
- }
Note : Minor GC 和 Full GC 差別
大物件直接進入老年代 :
所謂大物件指的是需要大量連續記憶體空間的 Java 物件, 大物件對 JVM 記憶體分配來說是一個壞消息, 經常出現大物件容易導致記憶體還有不少空間時就提前觸發 GC 以獲取足夠的空間來 "安置" 它們. JVM 提供了一個 -XX:PretenureSizeThreshold 參數, 令大於這個設置值的物件直接在老年代中分配, 這樣做的目的是避免再 Eden 區及兩個 Survivor 區發生大量的記憶體拷貝.
下面程式碼執行後, 我們看到 Eden 空間幾乎沒有被使用, 而老年代 10MB 空間被使用了 40%, 也就是 4MB 的記憶體就直接分配在老年代中, 這是因為 -XX:PretenureSizeThreshold 被設為 3MB (就是 3145728B), 因此超過 3MB 的對象都會直接在老年代中進行分配 :
- Ex3_4.java :
- package ch03;
- public class Ex3_4 {
- public static int _1MB = 1024*1024;
- /**
- * Goal : 大物件直接進入老年代
- * VM Args : -verbose:gc -XX:+PrintGCDetails -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:PretenureSizeThreshold=3145728
- * @param args
- */
- public static void main(String[] args) {
- System.out.printf("\t[Info] Allocate 4M memory...\n");
- byte[] allocation = new byte[ 4*_1MB ];
- }
- }
長期存活的物件將進入老年代 :
JVM 既然採用分代收集的想法來管理記憶體, 在記憶體回收時就必須能識別那些對象應該放在新生代, 那些物件應該放在老年代. 為了做到這一點, JVM 給每個物件定義一個物件年齡計數器. 如果對象在 Eden 出生並經過第一次 Minor GC 後仍然活著並且能被 Survivor 容納, 將移動到 Survivor 空間中, 並將對象年齡加一. 當它年齡增加到一定程度 (預設 15 歲), 就會被晉升到老年代中, 這樣的行為可以透過參數 -XX:MaxTenuringThreshold 來設置 :
- Ex3_5.java :
- package ch03;
- import java.util.ArrayList;
- import java.util.List;
- public class Ex3_5 {
- public static int _1MB = 1024*1024;
- public static int _1KB = 1024;
- /**
- * Goal : 長期存活的物件將進入老年代
- * VM Args : -verbose:gc -XX:+PrintGCDetails -Xms20M -Xmx20M -Xmn10M
- * -XX:SurvivorRatio=8
- * -XX:+PrintTenuringDistribution
- * -XX:MaxTenuringThreshold=2
- * (-XX:+UseSerialGC)
- * @param args
- */
- public static void main(String[] args) throws Exception{
- List<byte[]> mbList = new ArrayList<byte[]>();
- mbList.add(new byte[1 * _1MB]);
- for(int i=0; i<2; i++) // Change looping time to affect Minor GC time.
- {
- byte[] alloc1 = new byte[6 * _1MB];;
- byte[] alloc2 = new byte[2 * _1MB];
- alloc1 = null; alloc2=null;
- }
- System.gc(); // Force Major GC
- }
- }
動態物件年齡判定 :
為了能更好適應不同程式的記憶體狀況, JVM 並不總是要求物件年齡必須達到 MaxTenuringThreshold 才能晉升老年代, 如果在 Survivor 空間中相同年齡所有物件大小的總和大於 Survivor 空間的一半, 年齡大於或等於該年齡的物件就可以直接進入老年代, 無須等到 MaxTenuringThreshold 中要求的年齡.
空間分配擔保 :
在發生 Minor GC 時, JVM 會檢測之前每次晉升到老年代的平均大小是否大於老年代的剩餘空間大小, 如果大於則改為直接進行一次 Full GC ; 如果小於, 則查詢 HandlePromotionFailure 設置是否允許擔保失敗, 如果允許則只會進行 Minor GC ; 如果不允許, 則改為進行一次 Full GC.
沒有留言:
張貼留言