jvm對大物件分配記憶體的特殊處理
前段日子在和leader交流技術的時候,偶然聽到jvm在分配記憶體空間給大物件時,如果young區空間不足會直接在old區切一塊過去。對於這個結論很好奇,也比較懷疑,所以就上網搜了下,發現還真有這麼回事。所謂的大物件是指,需要大量連續記憶體空間的Java物件,最典型的大物件就是那種很長的字串以及陣列(筆者列出的例子中的byte[]陣列就是典型的大物件)。大物件對虛擬機器的記憶體分配來說就是一個壞訊息(替Java虛擬機器抱怨一句,比遇到一個大物件更加壞的訊息就是遇到一群“朝生夕滅”的“短命大物件”,寫程式的時候應當避免),經常出現大物件容易導致記憶體還有不少空間時就提前觸發垃圾收集以獲取足夠的連續空間來“安置”它們。以下給出具體程式碼來說明:首先定義好jvm記憶體各個區域的大小。我設定的是eden區8M,from和to各1M,old區10M,總共20M的空間,引數如下:
- -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8
緊接著,開始寫程式。很簡單,就是初始化一個9M的程式,然後用jstat命令看jdk的記憶體使用情況。
public class App { private static final int _1MB = 1024 * 1024; /** * VM引數:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 * -XX:PretenureSizeThreshold=3145728 */ public static void main(String[] args) { byte[] allocation = new byte[9*_1MB]; while(true){ try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }
然後打成jar,執行。結果如下:
S0 S1 E O P YGC YGCT FGC FGCT GCT 0.00 0.00 18.04 90.00 23.08 0 0.000 20 0.027 0.027
果然,當物件大小大於eden區的時候會直接扔到old區。但我還不滿足與此,於是將物件改大了些,改成了11M。再次嘗試發現結果如下:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at com.taobao.jdkmem.App.main(App.java:17)
到這裡結束了麼?當然沒有:)這個是一個大的完整的物件,當大物件本身是由一連串的小物件組成的時候,會不會不再OOM呢?於是改了程式碼再次嘗試:
public class App {
private static final int _1MB = 1024 * 1024;
/**
* VM引數:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8
* -XX:PretenureSizeThreshold=3145728
*/
public static void main(String[] args) {
byte[][] allocation;
allocation = new byte[11][_1MB]; // 直接分配在老年代中
while(true){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
再次執行,結果如下:
S0 S1 E O P YGC YGCT FGC FGCT GCT
0.00 38.06 67.68 60.02 23.10 1 0.007 14 0.012 0.019
果然,這次居然又被jvm給生吃下去了。不過這次並非所有的都在old區,而是有一部分還在young區裡活著。看來jvm還是足夠彪悍的。
由此可見,當出現大物件的時候,jvm用犧牲部分寶貴的old區的方式來保證了整個jvm的正常運轉。所以,程式中儘量要避免大物件,如果實在不行,就讓大物件活的儘量久些,莫要new一個然後gc掉再new一個再gc,這麼爆jvm可不太友好。
到這裡結束了吧?你猜對了,還沒有:P既然知道jvm會對大物件申請記憶體做特殊處理,那麼就在琢磨程式設計師有沒有方法干預這個過程呢?答案是有的,就是使用這個引數-XX:PretenureSizeThreshold。這個引數的單位是Byte,其作用是當新物件申請的記憶體空間大於這個引數值的時候,直接扔到old區。做個試驗就證明了:
public class App {
private static final int _1MB = 1024 * 1024;
/**
* VM引數:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8
* -XX:PretenureSizeThreshold=3145728
*/
public static void main(String[] args) {
byte[] allocation = new byte[4*_1MB];
while(true){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
執行命令如下:
- java -jar -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:PretenureSizeThreshold=3145728 memtest-1.0-SNAPSHOT.jar
我設定的閾值是3M,意味著超過3M的物件會被直接扔到old區。結果是皆大歡喜,新物件直接被扔到了old區:
- S0 S1 E O P YGC YGCT FGC FGCT GCT
- 0.00 0.00 18.04 40.00 23.08 0 0.000 0 0.000 0.000
試驗有了結果,自然而然心情愉悅。但這個引數使用時需要慎重,因為fullgc的代價很高,因此old區就顯得非常寶貴。除非你真的清楚你在幹什麼,否則莫要輕易玩這個引數,萬一搞個頻繁fullgc就玩大了。ok,到此打完收工。