前言

     OOM,全称“Out Of Memory”,称之为内存溢出.翻译成中文就是“内存用完了”,当JVM因为没有足够的内存来为对象分配空间并且垃圾回收器也已经没有空间可回收时,就会抛出这个error


叙述

产生内存溢出的原因:
① 分配的少了:比如虚拟机本身可使用的内存(一般通过启动时的VM参数指定)太少。
② 应用用的太多,并且用完没释放,浪费了。此时就会造成内存泄露或者内存溢出。

内存区间

在JVM中,有哪些内存区间?
深入JVM-Java堆分析

堆溢出

GC Roots 到对象之间有可达路径来避免垃圾回收机制,清理这些对象就会在对象数量达到最大堆的绒容量 然后产生内存溢出异常.
创建对象时如果没有可以分配的堆内存,JVM就会抛出OutOfMemoryError:java heap space异常。

 /**
     * 堆溢出
     * @param args -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
     */
     public static void main(String[] args) {
        List<byte[]> list = new ArrayList<>();
        int i=0;
        while(true){
            list.add(new byte[5*1024*1024]);
            System.out.println("分配次数:"+(++i));
        }
    }

运行结果:
深入JVM-Java堆分析

永久区

    永久代溢出可以分为两种情况,第一种是常量池溢出,第二种是方法区溢出。
1、永久代溢出——常量池溢出
要模拟常量池溢出,可以使用String对象的intern()方法。如果常量池包含一个此String对象的字符串,就返回代表这个字符串的String对象,否则将String对象包含的字符串添加到常量池中。

public class ConstantPoolOOMTest {
 
    /**
     * VM Args:-XX:PermSize=10m -XX:MaxPermSize=10m
     * @param args
     */
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        int i=1;
        try {
            while(true){
                list.add(UUID.randomUUID().toString().intern());
                i++;
            }
        } finally {
            System.out.println("运行次数:"+i);
        }
    }
}

因为在JDK1.7中,当常量池中没有该字符串时,JDK7的intern()方法的实现不再是在常量池中创建与此String内容相同的字符串,而改为在常量池中记录Java Heap中首次出现的该字符串的引用,并返回该引用。
简单来说,就是对象实际存储在堆上面,所以,让上面的代码一直执行下去,最终会产生堆内存溢出。
下面我将堆内存设置为:-Xms5m -Xmx5m,执行上面的代码,运行结果如下:

运行次数:58162
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    at java.lang.Long.toUnsignedString(Unknown Source)
    at java.lang.Long.toHexString(Unknown Source)
    at java.util.UUID.digits(Unknown Source)
 
    at java.util.UUID.toString(Unknown Source)
    at com.ghs.test.ConstantPoolOOMTest.main(ConstantPoolOOMTest.java:18)

2、永久代溢出——方法区溢出
方法区存放Class的相关信息,下面借助CGLib直接操作字节码,生成大量的动态类。

public class MethodAreaOOMTest {
 
    public static void main(String[] args) {
        int i=0;
        try {
            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();
                i++;
            }
        } finally{
            System.out.println("运行次数:"+i);
        }
    }
 
    static class OOMObject{
 
    }
}
 
运行结果:
 
运行次数:56
Exception in thread "main" 
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "main"

Java栈溢出

    这里的栈溢出指,在创建线程的时候,需要为线程分配栈空间,这个栈空间是向操作系统请求的,如果操作系统无法给出足够的空间,就会抛出OOM
深入JVM-Java堆分析
栈空间不足时,需要分下面两种情况处理:

  • 线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError
  • 虚拟机在扩展栈深度时无法申请到足够的内存空间,将抛出OutOfMemberError
    附:当前大部分的虚拟机栈都是可动态扩展的。
  1. 栈空间不足——StackOverflowError实例
public class StackOverflowError {

    /**
     * 栈溢出
     * @param args
     */
    public static void main(String[] args){
        new StackOverflowError().test();
    }

    public void test(){
        test();
    }

}

运行结果:
深入JVM-Java堆分析
2. 栈空间不足——OutOfMemberError实例
    单线程情况下,不论是栈帧太大还是虚拟机栈容量太小,都会抛出StackOverflowError,导致单线程情境下模拟栈内存溢出不是很容易,不过通过不断的建立线程倒是可以产生内存溢出异常。

public class OutOfMemberError {
    int depth = 0;

    public void sofMethod(){
        depth ++ ;
        sofMethod();
    }

    public static void main(String[] args) {
        OutOfMemberError test = null;
        try {
            test = new OutOfMemberError();
            test.sofMethod();
        } finally {
            System.out.println("递归次数:"+test.depth);
        }
    }

}

深入JVM-Java堆分析

直接内存溢出

ByteBuffer.allocateDirect()无法从操作系统获得足够的空间
深入JVM-Java堆分析
DirectMemory可以通过-XX:MaxDirectMemorySize指定,如果不指定,默认与Java堆的最大值(-Xmx指定)一样。
NIO会使用到直接内存,你可以通过NIO来模拟,在下面的例子中,跳过NIO,直接使用UnSafe来分配直接内存。

public class DirectMemoryOOMTest {
    /**
     * VM Args:-Xms20m -Xmx20m -XX:MaxDirectMemorySize=10m
     * @param args
     */
    public static void main(String[] args) {
        int i=0;
        try {
            Field field = Unsafe.class.getDeclaredFields()[0];
            field.setAccessible(true);
            Unsafe unsafe = (Unsafe) field.get(null);
            while(true){
                unsafe.allocateMemory(1024*1024);
                i++;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            System.out.println("分配次数:"+i);
        }
    }
}

运行结果:
深入JVM-Java堆分析

OOM情况解析
**最常见的OOM情况有以下三种:**
  1. java.lang.OutOfMemoryError: Java heap space ------>java堆内存溢出,此种情况最常见,一般由于内存泄露或者堆的大小设置不当引起。对于内存泄露,需要通过内存监控软件查找程序中的泄露代码,而堆大小可以通过虚拟机参数-Xms,-Xmx等修改。
  2. java.lang.OutOfMemoryError: PermGen space ------>java永久代溢出,即方法区溢出了,一般出现于大量Class或者jsp页面,或者采用cglib等反射机制的情况,因为上述情况会产生大量的Class信息存储于方法区。此种情况可以通过更改方法区的大小来解决,使用类似-XX:PermSize=64m -XX:MaxPermSize=256m的形式修改。另外,过多的常量尤其是字符串也会导致方法区溢出。
  3. java.lang.StackOverflowError ------> 不会抛OOM error,但也是比较常见的Java内存溢出。JAVA虚拟机栈溢出,一般是由于程序中存在死循环或者深度递归调用造成的,栈大小设置太小也会出现此种溢出。可以通过虚拟机参数-Xss来设置栈的大小。

小结

栈内存溢出:程序所要求的栈深度过大。
堆内存溢出: 分清内存泄露还是 内存容量不足。泄露则看对象如何被 GC Root 引用,不足则通过调大-Xms,-Xmx参数。
永久代溢出:Class对象未被释放,Class对象占用信息过多,有过多的Class对象。
直接内存溢出:系统哪些地方会使用直接内存。

参考:https://blog.csdn.net/oqkdws/article/details/83412885

感谢您的阅读~~

相关文章: