HeapDumpOnOutOfMemoryError堆转储实践和一些分析

 

 

https://blog.csdn.net/u010398771/article/details/98058255

 

 

使用了标志-XX:+HeapDumpOnOutOfMemoryError,JVM会在遇到OutOfMemoryError时拍摄一个“堆转储快照”,并将其保存在一个文件中。

对如下一段代码,【代码1】 

/**
 * @author zk
 * @Description:
 * @date 2019-08-01 14:17
 */
public class Demo {
 
    public static void main(String[] args) {
        long arr[];
        for (int i=1; i<=10000000; i*=2) {
            arr = new long[i];
        }
    }
}

设置虚拟机参数为:-Xmx40m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=E:\Java\dump

执行程序,很快会抛出异常:

java.lang.OutOfMemoryError: Java heap space
Dumping heap to E:\Java\dump\java_pid8420.hprof ...
Heap dump file created [2131459 bytes in 0.097 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at com.curefun.vspmanagercaseedit.controller.Demo.main(Demo.java:13)

 

使用eclipse memory analyzer 打开E:\Java\dump\java_pid8420.hprof 这个文件,就能获得堆的信息了

奇怪的是Heap dump file只有一兆多一点,用JProfiler打开这个文件,并没有看到导致内存溢出的long[];刚开始以为是堆转储时,JVM会忽略掉只被线程栈引用的数组,进一步测试,发现并不是这个原因;查看相应的class文件,反编译后得到:

【将代码1通过JDK1.8编译后的字节码反编译后得到的代码】

 

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
 
package com.curefun.vspmanagercaseedit.controller;
 
public class Demo {
    public Demo() {
    }
 
    public static void main(String[] args) {
        for(int i = 1; i <= 10000000; i *= 2) {
            long[] arr = new long[i];
        }
    }
}

       其中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];的后面加上显示堆内存使用的语句: 

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

可以看出每次执行arr = new long[i];后堆内存的使用情况;配置-Xmx40m的情况下,抛出异常前最后一次正常执行的循环的输出信息为:

 

size : 65536
maxMemory : 38.50M
totalMemory : 38.50M
freeMemory : 33.15M
size : 131072
maxMemory : 38.50M
totalMemory : 38.50M
freeMemory : 32.15M
size : 262144
maxMemory : 38.50M
totalMemory : 38.50M
freeMemory : 30.15M
size : 524288
maxMemory : 38.50M
totalMemory : 38.50M
freeMemory : 32.94M
size : 1048576
maxMemory : 38.50M
totalMemory : 38.50M
freeMemory : 24.94M
size : 2097152
maxMemory : 38.50M
totalMemory : 38.50M
freeMemory : 8.94M
java.lang.OutOfMemoryError: Java heap space
Dumping heap to E:\Java\dump\java_pid10284.hprof ...
Heap dump file created [2557775 bytes in 0.033 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at com.curefun.vspmanagercaseedit.controller.Demo.main(Demo.java:13)

 

所有代码编译之后的为:

 

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
 
package com.curefun.vspmanagercaseedit.controller;
 
public class Demo {
    public Demo() {
    }
 
    public static void main(String[] args) {
        for(int i = 1; i <= 10000000; i *= 2) {
            long[] arr = new long[i];
            System.out.println("size : " + i);
            Runtime runtime = Runtime.getRuntime();
            System.out.printf("maxMemory : %.2fM\n", (double)runtime.maxMemory() * 1.0D / 1024.0D / 1024.0D);
            System.out.printf("totalMemory : %.2fM\n", (double)runtime.totalMemory() * 1.0D / 1024.0D / 1024.0D);
            System.out.printf("freeMemory : %.2fM\n", (double)runtime.freeMemory() * 1.0D / 1024.0D / 1024.0D);
        }
 
    }
}

  

将代码1改为:【代码2】

 

/**
 * @author zk
 * @Description:
 * @date 2019-08-01 14:31
 */
public class Demo2 {
 
    public static void main(String[] args) {
        long arr[] = {};
        for (int i=1; i<=10000000; i*=2) {
            arr = new long[i];
 
            System.out.println("size : " + i);
            Runtime runtime = Runtime.getRuntime();
            System.out.printf("maxMemory : %.2fM\n", runtime.maxMemory()*1.0/1024/1024);
            System.out.printf("totalMemory : %.2fM\n", runtime.totalMemory()*1.0/1024/1024);
            System.out.printf("freeMemory : %.2fM\n", runtime.freeMemory()*1.0/1024/1024);
        }
    }
 
}

 

jdk1.8编译之后的文件为:

 

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
 
package com.curefun.vspmanagercaseedit.controller;
 
public class Demo2 {
    public Demo2() {
    }
 
    public static void main(String[] args) {
        long[] arr = new long[0];
 
        for(int i = 1; i <= 10000000; i *= 2) {
            arr = new long[i];
            System.out.println("size : " + i);
            Runtime runtime = Runtime.getRuntime();
            System.out.printf("maxMemory : %.2fM\n", (double)runtime.maxMemory() * 1.0D / 1024.0D / 1024.0D);
            System.out.printf("totalMemory : %.2fM\n", (double)runtime.totalMemory() * 1.0D / 1024.0D / 1024.0D);
            System.out.printf("freeMemory : %.2fM\n", (double)runtime.freeMemory() * 1.0D / 1024.0D / 1024.0D);
        }
 
    }
}

其中long[] arr 的定义跟源代码一致,是在循环外面,应该是因为有初始化的代码,没办法再把定义移到循环里面;这种情况下,arr的生存范围是从声明到程序结束;每次循环开始时,会在堆中创建一个指定大小的long[],把它的引用赋给arr,这样arr之前指向的long[]就变成没有被引用的实例;进入了下次循环,在堆中创建一个指定大小的long[],把它的引用赋给arr,在赋值完成前,arr指向上次循环创建的long[]实例,赋值完成后,arr就指向本次循环创建的long[]实例,这时候上次循环创建的long[]实例就变成没有被引用的实例。

设置虚拟机参数为:-Xmx40m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=E:\Java\dump

 

执行程序,很快会抛出异常:

 

size : 131072
maxMemory : 38.50M
totalMemory : 38.50M
freeMemory : 32.15M
size : 262144
maxMemory : 38.50M
totalMemory : 38.50M
freeMemory : 30.15M
size : 524288
maxMemory : 38.50M
totalMemory : 38.50M
freeMemory : 30.84M
size : 1048576
maxMemory : 38.50M
totalMemory : 38.50M
freeMemory : 22.84M
size : 2097152
maxMemory : 38.50M
totalMemory : 38.50M
freeMemory : 6.84M
java.lang.OutOfMemoryError: Java heap space
Dumping heap to E:\Java\dump\java_pid5000.hprof ...
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
Heap dump file created [19230379 bytes in 0.112 secs]
	at com.curefun.vspmanagercaseedit.controller.Demo2.main(Demo2.java:13)

 

Heap dump file有十多兆,用JProfiler打开这个文件,可以找到一个占用16M内存的long[]。

分析:在程序因为没有足够的堆内存创建实例而抛出OutOfMemoryError时,引用arr仍然指向上次循环创建的long[]实例,在JVM将堆内存转储到文件中时,会把这个long[]实例也考虑进去;这个long[]实例被arr引用,arr位于线程栈中,所以上图中显示long[]实例被java stack引用。

 

在源代码arr = new long[i];的后面加上显示堆内存使用的语句,

可以看到抛出异常前最后一次正常执行的循环的输出信息为:

size : 2097152
maxMemory : 38.50M
totalMemory : 38.50M
freeMemory : 6.84M

 

最后一次正常创建的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时生成的堆转储文件,有助于找到内存不够用的原因;如果生成的堆转储文件的大小跟最大堆内存的配置有很大差异,就要分析抛出异常的代码,查找原因,还可以将字节码反编译,看看编译器是不是对代码的结构进行了调整。
————————————————
版权声明:本文为CSDN博主「长河」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/u010398771/article/details/98058255

posted @ 2022-11-11 14:25  kelelipeng  阅读(289)  评论(0编辑  收藏  举报