本文来说下常见内存溢出异常


概述

在JVM内存区域中,除了程序计数器外,Java虚拟机的其他运行时区域都有可能发生OutOfMemoryError的异常,下面分别给出验证:

在这里插入图片描述


Java堆溢出

Java堆能够存储对象实例。通过不断地创建对象,并保证GC Roots到对象有可达路径来避免垃圾回收机制清除这些对象。
当对象数量到达最大堆的容量限制时就会产生OutOfMemoryError异常。

设置JVM启动参数:-Xms20M设置堆的最小内存为20M,-Xmx20M设置堆的最大内存和最小内存一样,这样可以防止Java堆在内存不足时自动扩容。-XX:+HeapDumpOnOutOfMemoryError参数可以让虚拟机在出现内存溢出异常时Dump出内存堆运行时快照。

在这里插入图片描述

HeapOOM.java

/**
 * VM Args: -Xms20M -Xmx20M -XX:+HeapDumpOnOutOfMemoryError
 */
public class HeapOOM {
    public static class OOMObject {
    }

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

测试运行结果:

在这里插入图片描述

打开Java VisualVM导出Heap内存运行时的dump文件

在这里插入图片描述
HeapOOM对象不停地被创建,堆内存使用达到99%。垃圾回收器不断地尝试回收但都以失败告终

分析:遇到这种情况,通常要考虑内存泄露和内存溢出两种可能性。

如果是内存泄露:

进一步使用Java VisualVM工具进行分析,查看泄露对象是通过怎样的路径与GC Roots关联而导致垃圾回收器无法回收的。

如果是内存溢出

通过Java VisualVM工具分析,不存在泄露对象,也就是说堆内存中的对象必须得存活着。就要考虑如下措施:

  1. 从代码上检查是否存在某些对象生命周期过长、持续状态时间过长的情况,尝试减少程序运行期的内存。
  2. 检查虚拟机的堆参数(-Xmx与-Xms),对比机器的物理内存看是否还可以调大。

虚拟机和本地方法栈溢出

关于虚拟机栈和本地方法栈,分析内存异常类型可能存在以下两种:

  • 如果现场请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常
  • 如果虚拟机在扩展栈时无法申请到足够的内存空间,可能会抛出OutOfMemoryError异常

可以划分为两类问题,当栈空间无法分配时,到底时栈内存太小,还是已使用的栈内存过大。


StackOverflowError异常

  1. 使用-Xss参数减少栈内存的容量,异常发生时打印栈的深度。
  2. 定义大量的本地局部变量,以达到增大栈帧中的本地变量表的长度。

设置JVM启动参数:-Xss128k设置栈内存的大小为128k

在这里插入图片描述

JavaVMStackSOF.java

/**
 * VM Args: -Xss128k
 */
public class JavaVMStackSOF {
    private int stackLength = 1;

    private void stackLeak() {
        stackLength++;
        stackLeak();
    }

    public static void main(String[] args) {
        JavaVMStackSOF oom = new JavaVMStackSOF();
        try {
            oom.stackLeak();
        } catch (Throwable e) {
            System.out.println("Stack length: " + oom.stackLength);
            throw e;
        }
    }
}

测试结果:

在这里插入图片描述

分析:在单个线程下,无论是栈帧太大还是虚拟机栈容量太小,当无法分配内存的时候,虚拟机抛出的都是StackOverflowError异常


OutOfMemoryError异常

  1. 不停地创建线程并保持线程运行状态。

JavaVMStackOOM.java

/**
 * VM Args: -Xss2M
 */
public class JavaVMStackOOM {
    private void running() {
        while (true) {
        }
    }

    public void stackLeakByThread() {
        while (true) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    running();
                }
            }).start();
        }
    }

    public static void main(String[] args) {
        JavaVMStackOOM oom = new JavaVMStackOOM();
        oom.stackLeakByThread();
    }
}

测试结果:

Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread

上述测试代码运行时存在较大的风险,可能会导致操作系统假死,这里就不亲自测试了,引用作者的测试结果。


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

运行时常量池内存溢出测试

运行时常量和字面量都存放于运行时常量池中,常量池又是方法区的一部分,因此两个区域的测试是一样的。 这里采用String.intern()进行测试:

String.intern()是一个native方法,它的作用是:如果字符串常量池中存在一个String对象的字符串,那么直接返回常量池中的这个String对象; 否则,将此String对象包含的字符串放入常量池中,并且返回这个String对象的引用。

设置JVM启动参数:通过-XX:PermSize=10M和-XX:MaxPermSize=10M限制方法区的大小为10M,从而间接的限制其中常量池的容量。

在这里插入图片描述

RuntimeConstantPoolOOM.java

/**
 * VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M
 */
public class RuntimeConstantPoolOOM {

    public static void main(String[] args) {
        // 使用List保持着常量池的引用,避免Full GC回收常量池
        List<String> list = new ArrayList<>();
        // 10MB的PermSize在Integer范围内足够产生OOM了
        int i = 0;
        while (true) {
            list.add(String.valueOf(i++).intern());
        }
    }
}

测试结果分析:

JDK1.6版本运行结果

Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
    at java.lang.String.intern(Native Method)

JDK1.6版本运行结果显示常量池会溢出并抛出永久带的OutOfMemoryError异常。 而JDK1.7及以上的版本则不会得到相同的结果,它会一直循环下去。


方法区内存溢出测试

方法区存放Class相关的信息,比如类名、访问修饰符、常量池、字段描述、方法描述等。对于方法区的内存溢出的测试,基本思路是在运行时产生大量类字节码区填充方法区。

这里引入Spring框架的CGLib动态代理的字节码技术,通过循环不断生成新的代理类,达到方法区内存溢出的效果。

JavaMethodAreaOOM.java

/**
 * VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M
 */
public class JavaMethodAreaOOM {

    public static void main(String[] args) {
        while (true) {
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(OOMObject.class);
            enhancer.setUseCache(false);
            enhancer.setCallback(new MethodInterceptor() {
                @Override
                public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
                    return proxy.invokeSuper(obj, args);
                }
            });

            enhancer.create();
        }
    }

    private static class OOMObject {
        public OOMObject() {
        }
    }
}

JDK1.6版本运行结果:

Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
    at java.lang.ClassLoader.defineClass1(Native Method)
    at java.lang.ClassLoader.defineClassCond(ClassLoader.java:632)
    at java.lang.ClassLoader.defineClass(ClassLoader.java:616)

测试结果分析:

JDK1.6版本运行结果显示常量池会溢出并抛出永久带的OutOfMemoryError异常。 而JDK1.7及以上的版本则不会得到相同的结果,它会一直循环下去


直接内存溢出

本机直接内存的容量可通过-XX:MaxDirectMemorySize指定,如果不指定,则默认与Java堆最大值(-Xmx指定)一样

测试场景

直接通过反射获取Unsafe实例,通过反射向操作系统申请分配内存:

设置JVM启动参数:-Xmx20M指定Java堆的最大内存,-XX:MaxDirectMemorySize=10M指定直接内存的大小。

在这里插入图片描述

DirectMemoryOOM.java

/**
 * VM Args: -Xmx20M -XX:MaxDirectMemorySize=10M
 */
public class DirectMemoryOOM {

    private static final int _1MB = 1024 * 1024;

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

测试结果:

在这里插入图片描述

测试结果分析:

由DirectMemory导致的内存溢出,一个明显的特征是Heap Dump文件中不会看到明显的异常信息。 如果OOM发生后Dump文件很小,并且程序中直接或者间接地使用了NIO,那么就可以考虑一下这方面的问题。


参考

  1. 周志明,深入理解Java虚拟机:
  2. JVM高级特性与最佳实践,机械工业出版社

本文小结

本文详细介绍了常见的内存溢出异常。

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐