您的位置 首页 java

JVM内存溢出的四种场景 实战分析

OutOfMemoryError异常相信对很多程序员来说一定很头疼,平时不常遇到,但是遇到时却无从下手,其实不论什么样的问题,都有决绝的规律和方法。

OutOfMemoryError异常主要包括:

1、Java堆溢出

2、Java栈溢出

3、方法区和运行时常量池溢出

4、本机直接内存溢出

Java堆溢出

我们都知道,JVM中三大区:堆区、栈区、方法区,其中堆区发生溢出的可能性尤其大,发生溢出大多是代码编写问题,我们现在就模拟一个Java堆溢出的场景。

代码如下:

 import java.util.List;

public class HeapOOM {

    static class OOMObject {

    }

    public static void main(String[] args) {
        List<OOMObject> list = new ArrayList<>();
        while(true) {
            list.add(new OOMObject());
        }
    }
}  

运行代码之前,需要先修改JVM的参数配置,如下:

 -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError  

-Xms20m -Xmx20m 将堆最大最小内存都设置为20m

-XX:+HeapDumpOnOutOfMemoryError 是在虚拟机发生异常时,dump出当前内存信息,用于分析问题。

修改好JVM运行参数后,执行代码。

报错如下:

 java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid17932.hprof ...
Heap dump file created [28214773 bytes in 0.053 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3210)
at java.util.Arrays.copyOf(Arrays.java:3181)
at java.util.ArrayList.grow(ArrayList.java:265)
at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:239)
at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:231)
at java.util.ArrayList.add(ArrayList.java:462)
at com.itzhimei.study.jvm.HeapOOM.main(HeapOOM.java:16)  

原因很明显,就是堆内存满了,后续新建的对象无法申请到内存空间,所以报了OutOfMemoryError的异常。

如果是平时,遇到这种情况我们可以用工具来分析异常发生时的dump文件,这里使用Memory Analyzer Tools来分析。

JVM内存溢出的四种场景 实战分析

从图中可以看出,从根开始,往下有一个持有了大量对象的Objec,里面都是创建的OOMObject类的对象,就是我们代码里定义list所持有的。

这里还需要说明的两个点,就是图中列表里的Shallow Heap和Retained Heap

  1. Shallow Heap 表示当前对象占用的内存大小
  2. Retained Heap 表示当前对象及其能直接或间接访问到的对象,也就是发生垃圾回收时,能够释放的空间大小

Java栈溢出

栈溢出分调用超出最大深度溢出和内存不足溢出,分别对应:StackOverflowError和OutOfMemoryError。但这两种异常其实反应的是一个问题,那就是stack内存不足。

栈溢出的问题,有一定编程经验的开发者可能马上想到对应的场景,那就是无限制的递归,无限制递归也就是不停调用同一函数,压入栈中,最终导致栈内存不足,从而抛出StackOverflowError错误。

我们来模拟一下StackOverflowError

代码如下:

 /**
 * www.itzhimei.com
 * 编程技术之美-IT之美
 */public class StackSOF {
    private int len = 0;
    public void stackAdd() {
        len++;
        //调用自身,实现递归效果,只是这个递归没有退出条件,那么肯定会发生栈溢出
        stackAdd();
    }

    public static void main(String[] args) {
        StackSOF sof = new StackSOF();
        sof.stackAdd();
    }
}  

JVM参数:

 -Xss64k  
 Exception in thread "main" java.lang.StackOverflowError
at com.itzhimei.study.jvm.StackSOF.stackAdd(StackSOF.java:10)
at com.itzhimei.study.jvm.StackSOF.stackAdd(StackSOF.java:12)
at com.itzhimei.study.jvm.StackSOF.stackAdd(StackSOF.java:12)
at com.itzhimei.study.jvm.StackSOF.stackAdd(StackSOF.java:12)
at com.itzhimei.study.jvm.StackSOF.stackAdd(StackSOF.java:12)
at com.itzhimei.study.jvm.StackSOF.stackAdd(StackSOF.java:12)
at com.itzhimei.study.jvm.StackSOF.stackAdd(StackSOF.java:12)
at com.itzhimei.study.jvm.StackSOF.stackAdd(StackSOF.java:12)
at com.itzhimei.study.jvm.StackSOF.stackAdd(StackSOF.java:12)
at com.itzhimei.study.jvm.StackSOF.stackAdd(StackSOF.java:12)
        ......
        ......
        at com.itzhimei.study.jvm.StackSOF.main(StackSOF.java:17)  

方法区和运行时常量池溢出

要模拟出方法区和运行时常量池的溢出,这里需要区分一下JDK的版本。

JDK1.6及之前的版本,常量池是方法区中的一部分,在JDK1.7开始,常量池被放到的heap中。

代码:

 import java.util.ArrayList;
import java.util.List;

/**
 * www.itzhimei.com
 * 编程技术之美-IT之美
 */public class ConstantPoolOOM {

    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        int i=0;
        while(true) {
            String s = "a"+i++;
            list.add(s.intern());
        }
    }
}  

JVM参数:

 -Xms10M -Xmx10M -XX:PermSize=5M -XX:MaxPermSize=5M  

报错如下:

 Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at com.itzhimei.study.jvm.ConstantPoolOOM.main(ConstantPoolOOM.java:12)
Java HotSpot(TM) 64-Bit Server VM warning: ignoring option PermSize=5M; support was removed in 8.0
Java HotSpot(TM) 64-Bit Server VM warning: ignoring option MaxPermSize=5M; support was removed in 8.0  

重点在这里:java.lang.OutOfMemoryError: Java heap space

报错是提示heap内存不足,因为我本地是JDK1.8

如果是JDK1.6或以下,则会提示java.lang.OutOfMemoryError:PermGen space,那么就表示方法区溢出了,有兴趣的可以用1.6试一试。

在JDK1.7及以后的版本中要让PermGen space内存溢出,那么需要使用类似spring的CGLIB这样的功能,在运行时动态生成大量class,编译成字节码,加载到JVM的方法区中,因为class字节码都是存放在方法区的,当动态生成的class过多,那么方法区自然就溢出了。

本机直接内存溢出

本机直接内存–Direct Memory,不是JVM内存中的一部分,但是这部分内存也被频繁的使用,并且可能会产生OutOfMemoryError。

从JDK1.4中新加入了NIO类,引入了基于channel和Buffer的I/O方式,它可以使用Native函数库直接分配堆外内存,也就是在我们系统内存上分配了使用空间,然后使用了Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。为的是提高读写性能。

直接内存可以通过:-XX:MaxDirectMemorySize 来设置大小,如果不设置,默认和堆在最大值-Xmx一样大。

设置本机直接内存的原则就是,各种内存大小+本机直接内存大小<机器物理内存。

代码:

 /**
 * www.itzhimei.com
 * 编程技术之美-IT之美
 */public class DirectMemoryOOM {
    private static final int SIZE_2MB = 1024*1024*2;

    public static void main(String[] args) throws Exception {
        Field uf = Unsafe.class.getDeclaredFields()[0];
        uf.setAccessible(true);
        Unsafe unsafe = (Unsafe)uf.get(null);
        while(true) {
            unsafe.allocateMemory(SIZE_2MB);
        }
    }
}  

JVM参数:

 -XX:MaxDirectMemorySize=10M  

报错如下:

 Exception in thread "main" java.lang.OutOfMemoryError
at sun.misc.Unsafe.allocateMemory(Native Method)
at com.itzhimei.study.jvm.DirectMemoryOOM.main(DirectMemoryOOM.java:19)  

上面的代码可能很多人看不太懂,这一对unsafe是什么意思?怎么就产生直接内存溢出了?

         Field uf = Unsafe.class.getDeclaredFields()[0];
        uf.setAccessible(true);
        Unsafe unsafe = (Unsafe)uf.get(null);
        while(true) {
            unsafe.allocateMemory(SIZE_2MB);
        }  

这就是我们大部分程序员并不会用到的Unsafe类。

因为要操作的是JVM之外的内存–直接内存,规范的操作是使用Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作,但是在NIO出现之前,操作直接内存,用的就是Unsafe类。

Unsafe类是什么?

Unsafe类使Java拥有了像C语言的指针一样操作内存空间的能力,同时也带来了指针的问题。过度的使用Unsafe类会使得出错的几率变大,因此Java官方并不建议使用的,官方文档也几乎没有。

知道了Unsafe的能力,所以上面的代码就实现直接内存的使用。也就最终导致直接内存溢出。

以上内容学习自周志明老师的《深入理解JVM虚拟机》

文章来源:智云一二三科技

文章标题:JVM内存溢出的四种场景 实战分析

文章地址:https://www.zhihuclub.com/183878.shtml

关于作者: 智云科技

热门文章

网站地图