原创

Java 实战 OutOfMemoryError 异常演示分析

目的

  1. 通过代码验证Java虚拟机规范中描述的各个运行时区域存储的内容。
  2. 希望读者在工作中遇到实际的内存溢出异常时,能根据异常信息快速判断是哪个内存区域发生内存溢出,知道什么样的代码可能会导致这些区域内存溢出,以及出现这些异常该如何处理。

1. Java 堆溢出

原理:Java堆用于存储对象实例,只要不断创建对象,并保证GC Roots到对象之间有可达路径来避免垃圾收回,当对象到达最大堆容量就是内存溢出。HeapDumpOnOutOfMemoryError指定设置虚拟机在出现内存溢出异常时 Dump 出当前的内存堆转储快照以便事后进行分析,HeapDumpPath指定快照输入路径。

虚拟机配置:

-Xms20M -Xmx20M -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/Users/weilai/Documents/java/test

代码:

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

/**
 * @author weilai
 * @email 352342845@qq.com
 * @date 2020/8/20 2:27 下午
 */
public class HeapOOM {

    static class OOMObject {

    }

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

        while (true) {
            list.add(new OOMObject());
        }

    }
}

运行结果:
一般日志会输出 java.lang.OutOfMemoryError: Java heap space

java.lang.OutOfMemoryError: Java heap space
Dumping heap to /Users/weilai/Documents/java/test/java_pid43324.hprof ...
Heap dump file created [27733611 bytes in 0.087 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

内存泄露分析:

  • 如果是内存泄露,可进一步通过工具查看泄漏对象到GC Roots的引用链。掌握泄漏对象的类型信息及GC Roots引用链信息,可以比较准确的定位泄漏代码的位置。
  • 如果不存在内存泄漏,一是可检查虚拟机参数(-Xmx、-Xms)是否分配不足,适当的可以调大。二是检查是否有对象存过周期过长、持有状态时间过长等减少内存使用。

使用JProfiler工具查看java_pid43324.hprof快照文件。

HeapOOM类instance count过多:

file

点进去查看内存使用情况:
file

2. 虚拟机栈和本地方法栈溢出

由于在 HotSpot 虚拟机中并不区分虚拟机栈和本地方法栈,因为,对于HotSpot来说,虽然 -Xoss 参数(设置本地方法栈大小)存在,但实际没有效果,栈容量只有 -Xss 参数(每个线程的堆栈大小)设置。

虚拟机栈和本地方法栈中的两种异常:

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

异常分两种,看似严谨,但是却存在重叠:当占空间无法分配时,到底是内存太小,还是已使用的栈空间太大,本质只是对同一件事情的两种描述。

代码实验中,如果仅仅使用单线程中操作,尝试下面两种方法都无法让虚拟机产生OutOfMemoryError,结果都是StackOverflowError。

  • 使用 -Xss 参数减少栈内存容量。抛出 StackOverflowError 异常,异常出现时输出的堆栈深度相应缩小。
  • 定义大量本地变量,增大此方法帧中本地变量表的长度,抛出 StackOverflowError 异常时输出堆栈深度相应缩小。

代码:

public class JavaVMStackOF {

    private int stackLength = 1;

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

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

运行结果:
虚拟机参数 -Xss256k

stack length:1890
Exception in thread "main" java.lang.StackOverflowError
    at com.weilai.invalley.JavaVMStackOF.stackLeak(JavaVMStackOF.java:26)
    ...

虚拟机参数 -Xss160k

stack length:773
Exception in thread "main" java.lang.StackOverflowError
    at com.weilai.invalley.JavaVMStackOF.stackLeak(JavaVMStackOF.java:26)
    ...

总结: 在单线程下,无论是由于栈帧太大还是虚拟机栈容量太小,当内存无法分配时,都是抛出 StackOverflowError。虚拟机抛出异常时,都会有对应的堆栈信息,找寻解决比较容易

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

运行时常量池是方法区的一部分,所以放在一起。一般会提示PermGen space

java.lang.OutOfMemoryError: PermGen space

3.1 运行时常量池

原理: String类的intern()方法可以将首次出现的字符串实例添加到常量池,并返回String对象引用(要是用jdk1.6或者更低,因为jdk1.7+只会将首次出现的字符串实例引用存到运行时常量池,基本不会内存溢出)。在通过 -XX:PermSize-XX:MaxPermSize-XX:MetaspaceSize-XX:MaxMetaspaceSize参数设置永久代或元空间大小限制常量池内存容量。

虚拟机参数:

-XX:PermSize=3M -XX:MaxPermSize=3M

代码:

public class RuntimeConstantPoolOOM {


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

运行结果:

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

jdk1.6与jdk1.7中String.intern()方法区别。

jdk1.6将首次出现的字符串实例添加到常量池,并返回String对象引用。jdk1.7+只会将首次出现的字符串实例引用存到运行时常量池

public static void main(String[] args) {
    String str1 = new StringBuilder("计算机").append("软件").toString();
    // jdk1.6 false jdk1.7+ true
    // 1.6 intern() 会将首次出现的由StringBuilder创建的字符串实例复制到永久代,str1.intern内存指向永久代,str1对象分配在堆上,所以为false
    // 1.7 intern() 不在复制实例,只是常量池中记录首次出现的实例引用,常量池中记录出现的地址,str1.intern()与str1都是堆地址,所以为true
    System.out.println(str1.intern() == str1);

    // false
    // java字符串在常量池中默认存在,str2.intern()得到常量池中引用,str2为堆中对象,所以为false。
    String str2 = new StringBuilder("ja").append("va").toString();
    System.out.println(str2.intern() == str2);
}

3.2 方法区

原理: 方法区存放 Class 的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。基本思路产生大量类填满方法区,直到内存溢出。使用工具为CGLib,地址:https://github.com/cglib/cglib例子并非纯实验,主流的Spring等框架对类进行增强时,也使用CGLib这类字节码技术

虚拟机参数:

-XX:PermSize=3M -XX:MaxPermSize=3M

代码:

public class JavaMethodAreaOOM {

    static class OOMObject {

    }

    static class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
        @Override
        public void uncaughtException(Thread t, Throwable e) {
            System.out.println("线程信息:" + t.toString());
            System.out.println("报错信息:" + e.getMessage());
        }
    }

    public static void main(String[] args) throws Throwable {
        Thread.setDefaultUncaughtExceptionHandler(new MyUncaughtExceptionHandler());
        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 methodProxy) throws Throwable {
                    return methodProxy.invokeSuper(obj, args);
                }
            });
            enhancer.create();
        }
    }
}

运行结果:

Cause by:java.lang.OutOfMemoryError: PermGen space
    ...

4. 本机直接内存溢出

原理:本机内存(DirectMemory)容量可以通过 -XX:MaxDirectMemorySize 指定,不指定默认与Java 堆最大值一样(-Xmx)。使用直接内存未使用DirectByteMemory类,因为它抛出异常并非真正向系统申请分配,而是通过计算得知无法分配,使用Unsafe.alloateMemory()直接真正向系统申请内存

虚拟机参数:

-Xmx20M -Xms20M -XX:MaxDirectMemorySize=5M

代码:

public class DirectMemoryOOM {

    private static final int _1MB = 1024 * 1024;

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

运行结果:

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

总结:由直接内存导致的内存溢出,明显特征为 Heap Dump 文件中看不出明显异常,OOM之后 Dump 文件很小,而程序使用了NIO等,可以检查是不是直接内存溢出

正文到此结束
本文目录