Skip to content

Java 内存中几种常见的OOM 异常

Published: at 16:41:33

在JAVA虚拟机规范说明中,除了程序计数器不会出现OutOfMemoryError(简称OOM),其他几个内存区域都有可能发生OOM异常。

JAVA堆溢出

JAVA堆用于存储对象实例,只要不断的new对象,并且保证GC roots到对象之间有可达路径来避免垃圾回收这些对象,在对象达到一定的数量之后,就会产生OOM异常。

示例:

  1. 使用JVM参数限制JAVA堆的大小

    -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:/var/log/gc_dump.dump

  2. JAVA程序代码示例:

    public class TestOOM{
        static class OOMObejct{
    
        }
        public static void main(String[] args){
            List<OOMObejct> list = new ArrayList<>();
            while(true){
                list.add(new OOMObejct());
            }
        }
    }
  3. 运行结果:

    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 net.admol.jingling.demo.jvm.TestOOM.main(TestOOM.java:19)

当异常信息出现“java.lang.OutOfMemoryError”,并且后面有提示”Java heap space”时,就是JAVA堆出现了内存溢出。

栈溢出

由于虚拟机中并不区分虚拟机栈和本地方法栈, 栈的容量通过JVM参数 -Xss 设置,栈异常有两种可能异常:

  1. 线程的栈请求深度大于虚拟机所允许的最大深度时,会抛出StackOverFlowError异常
  2. 虚拟机栈在扩展栈时无法申请到足够的空间,会抛出OutOfMemoryError异常

异常示例:

  1. JVM参数限制栈大小

    -Xss128k

  2. JAVA程序代码示例:

    public class TestJVMStackOME{
        private int length = 1;
        public void stackLeak(){
            length++;
            stackLeak();
        }
        public static void main(String[] args){
            TestJVMStackOME test = new TestJVMStackOME();
            try{
                test.stackLeak();
            }catch(Exception e){
                System.out.println("stack length:"+test.length);
                throw e;
            }
        }
    }
  3. 运行结果:

    Exception in thread "main" java.lang.StackOverflowError
    	at net.admol.jingling.demo.jvm.TestJVMStackOME.stackLeak(TestJVMStackOME.java:11)
    	at net.admol.jingling.demo.jvm.TestJVMStackOME.stackLeak(TestJVMStackOME.java:12)
      ......省略更多的输出

方法区溢出

因为运行时常量池是方法区的一部分,所以它们抛出的异常都是一样的。

String.intern()是一个Native方法,它的作用是:如果常量池中已经包含了一个等于此String对象的字符串,则返回代表池中这个字符串的String对象;否则,将此String 对象包含的字符串添加到常量池中,并且返回此String对象的引用。

在JDK1.6及之前,由于常量池分配在永久代内,我们可以通过-XX:PermSize 和 -XX:MaxPermSize 参数来限制方法区大小,从而间接限制常量池的大小。

示例:

  1. 设置JVM参数限制方法区大小

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

  2. JAVA程序代码示例:

    public class TestMethodOOM{
        public static void main(String[] args){
            List<String> list = new ArrayList<>();
            int i = 100;
            while(true){
                list.add((String.valueOf(i++).intern()));
            }
        }
    }
  3. 在jdk1.6及之前会提示java.lang.OutOfMemoryError:PermGen space

    因为jdk1.8移除了PermGen(永久代),替换成了元空间(Metaspace),所以1.7及之后都不会报错

直接堆外内存溢出

DirectMemory容量可通过-XX:MaxDirectMemory 指定,如果不指定,则默认和JAVA堆最大值一致。

溢出示例:

  1. 设置JVM参数

    Xmx10m -XX:MaxDirectMemorySize=5M

  2. JAVA程序代码示例

    public class TestDirectMemoryOOM{
        public static void main(String[] args) throws IllegalAccessException{
            Field field = Unsafe.class.getDeclaredFields()[0];
            field.setAccessible(true);
            Unsafe unsafe = (Unsafe)field.get(null);
            long _1M = 1024*1024;
            while(true){
                unsafe.allocateMemory(_1M);
            }
        }
    }
  3. 运行结果

    Exception in thread "main" java.lang.OutOfMemoryError
    	at sun.misc.Unsafe.allocateMemory(Native Method)
    	at net.admol.jingling.demo.jvm.TestDirectMemoryOOM.main(TestDirectMemoryOOM.java:18)

因为DirectMemory导致的内存溢出, 一个明显的特征是在Heap Dump文件中不会看见明显的异常, 如果发现dump的文件比较小, 而程序中又直接或间接使用了NIO,那就需要好好思考一下是否是DirectMemory这方面的原因了。