HeapDumpOnOutOfMemoryError堆轉儲實踐和一些分析

 使用了標誌-XX:+HeapDumpOnOutOfMemoryError,JVM會在遇到OutOfMemoryError時拍攝一個「堆轉儲快照」,並將其保存在一個文件中。java

對以下一段代碼,【代碼1】數組

Java代碼  測試

  1. public static void main(String[] args) {  
  2.     long arr[];      
  3.     for (int i=1; i<=10000000; i*=2) {  
  4.         arr = new long[i];  
  5.     }  
  6. }  

 

      設置虛擬機參數爲:-Xmx40m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=E:\Java\dump優化

執行程序,很快會拋出異常:spa

java.lang.OutOfMemoryError: Java heap space線程

Dumping heap to E:\Java\dump\java_pid10400.hprof ...blog

Heap dump file created [1192880 bytes in 0.024 secs]內存

Exception in thread "main" java.lang.OutOfMemoryError: Java heap spaceget

 

    奇怪的是Heap dump file只有一兆多一點,用JProfiler打開這個文件,並無看到致使內存溢出的long[];剛開始覺得是堆轉儲時,JVM會忽略掉只被線程棧引用的數組,進一步測試,發現並非這個緣由;查看相應的class文件,反編譯後獲得:編譯器

【將代碼1經過JDK1.6編譯後的字節碼反編譯後獲得的代碼】

Java代碼  

  1. public static void main(String[] args)  
  2. {  
  3.     for (int i = 1; i <= 10000000; i *= 2) {  
  4.       long[] arr = new long[i];  
  5.     }  
  6. }  

 

其中long[] arr 的定義從循環外面變到了循環裏面,應該是編譯器進行了優化,這樣修改後,功能並無變化,但long[] arr的生存範圍變小了,生存範圍是從聲明到本次循環結束;每次循環開始時,在線程棧中聲明一個指向long[]的引用 arr,而後在堆中建立一個指定大小的long[],把它的引用賦給arr;在每次循環結束,進入下次循環前,線程棧中的引用arr就會被銷燬,它所指向的long[]就變成了沒有被引用的實例;進入了下次循環,又從新在線程棧中聲明一個引用arr,在堆中建立一個指定大小的long[],把它的引用賦給arr。

分析:虛擬機參數配置了-Xmx40m,在堆內存的使用量超過40M時,虛擬機就會拋出OutOfMemoryError: Java heap space,同時將堆內存轉儲到文件中,這時候前面循環建立的long[]實例沒有被引用,應該已經被垃圾回收,因此Heap dump file中沒有程序建立的long[]實例。

 

在源代碼arr = new long[i];的後面加上顯示堆內存使用的語句:

Java代碼  

  1. System.out.println("size : " + i);  
  2. Runtime runtime = Runtime.getRuntime();  
  3. System.out.printf("maxMemory : %.2fM\n", runtime.maxMemory()*1.0/1024/1024);  
  4. System.out.printf("totalMemory : %.2fM\n", runtime.totalMemory()*1.0/1024/1024);  
  5. System.out.printf("freeMemory : %.2fM\n", runtime.freeMemory()*1.0/1024/1024);  

能夠看出每次執行arr = new long[i];後堆內存的使用狀況;配置-Xmx40m的狀況下,拋出異常前最後一次正常執行的循環的輸出信息爲:

size : 4194304

maxMemory : 39.75M

totalMemory : 38.50M

freeMemory : 6.27M

 

將代碼1改成:【代碼2】

Java代碼  

  1. public static void main(String[] args) {  
  2.     long arr[] = {};   
  3.     for (int i=1; i<=10000000; i*=2) {  
  4.         arr = new long[i];  
  5.     }  
  6. }  

 

查看相應的class文件,反編譯後獲得:

【將代碼2經過JDK1.6編譯後的字節碼反編譯後獲得的代碼】

Java代碼  

  1. public static void main(String[] args)  
  2. {  
  3.     long[] arr = new long[0];  
  4.     for (int i = 1; i <= 10000000; i *= 2) {  
  5.       arr = new long[i];  
  6.     }  
  7. }  

 

    其中long[] arr 的定義跟源代碼一致,是在循環外面,應該是由於有初始化的代碼,沒辦法再把定義移到循環裏面;這種狀況下,arr的生存範圍是從聲明到程序結束;每次循環開始時,會在堆中建立一個指定大小的long[],把它的引用賦給arr,這樣arr以前指向的long[]就變成沒有被引用的實例;進入了下次循環,在堆中建立一個指定大小的long[],把它的引用賦給arr,在賦值完成前,arr指向上次循環建立的long[]實例,賦值完成後,arr就指向本次循環建立的long[]實例,這時候上次循環建立的long[]實例就變成沒有被引用的實例。

    設置虛擬機參數爲:-Xmx40m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=E:\Java\dump

執行程序,很快會拋出異常:

  java.lang.OutOfMemoryError: Java heap space

Dumping heap to E:\Java\dump\java_pid11020.hprof ...

Heap dump file created [17970188 bytes in 0.172 secs]

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

Heap dump file有十多兆,用JProfiler打開這個文件,能夠找到一個佔用16M內存的long[]。

 

 

 

分析:在程序由於沒有足夠的堆內存建立實例而拋出OutOfMemoryError時,引用arr仍然指向上次循環建立的long[]實例,在JVM將堆內存轉儲到文件中時,會把這個long[]實例也考慮進去;這個long[]實例被arr引用,arr位於線程棧中,因此上圖中顯示long[]實例被java stack引用。

 

    在源代碼arr = new long[i];的後面加上顯示堆內存使用的語句,

能夠看到拋出異常前最後一次正常執行的循環的輸出信息爲:

size : 2097152

maxMemory : 39.75M

totalMemory : 33.26M

freeMemory : 8.92M

    最後一次正常建立的long[]的size爲2097152,佔用了16M內存,而代碼1執行時最後一次正常建立的long[]的size爲4194304,佔用了32M內存。

分析:代碼1在循環中建立long[]實例時,上次循環建立的long[]實例沒有被引用,能夠被垃圾回收掉,因此在參數Xmx40m下,代碼1建立佔用32M內存的long[]仍是能夠正常執行的,試圖建立佔用64M內存的long[]才拋出異常;代碼2在循環中建立long[]實例時,上次循環建立的long[]實例還在被arr引用,不能被垃圾回收掉,代碼2在建立佔用16M內存的long[]實例時,前一個循環建立的佔8M內存的long[]實例還不能被回收,8+16=24 < 40,因此此次可以正常執行,下一個循環要嘗試建立佔32M內存的long[]實例,這時候佔16M內存的long[]實例還不能被回收,16+32=48>40,堆內存不夠用,只好拋出異常。

  

小結:經過分析OutOfMemoryError時生成的堆轉儲文件,有助於找到內存不夠用的緣由;若是生成的堆轉儲文件的大小跟最大堆內存的配置有很大差別,就要分析拋出異常的代碼,查找緣由,還能夠將字節碼反編譯,看看編譯器是否是對代碼的結構進行了調整。

 

http://epy.iteye.com/blog/1914455

相關文章
相關標籤/搜索