产生OutOfMemoryError异常的原因

时间:2021-01-18 20:55:21

最近在看周志明的《深入理解Java虚拟机》,觉得还是一本不错的书。对于和我一样对于JVM了解不深,有志进一步了解的人算是一本不错的书

1.设置虚拟机运行时参数

产生OutOfMemoryError异常的原因

2.造成内存溢出之五大元凶

2.1 Java堆溢出

Java堆用于存储对象实例,知道这一点就很容易呈现堆溢出,不断的创建对象,并且保持有指向其的引用,防止为gc。

代码如下:

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

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

    static class OOMObject{

    }

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

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

  }

通过设置-Xms20M -Xmx20M都为20M意在防止堆大小自动扩展,更好的展现溢出。 执行结果如下:

Exception in thread “main” java.lang.OutOfMemoryError: Java heap space
是不是很明显啊,显示堆空间发生OutOfMemoryError。

     书中告诉我们发生了OutOfMemoryError后,通常是通过内存影像分析工具对dump出来的堆转储快照进行分析(这就是运行时参数中配置-XX:+HeapDumpOnOutOfMemoryError的原因),重点是确定是由内存泄露(Memory Leak)还是有内存溢出(Memory Overflow)引起的OutOfMemoryError。如果是内存泄露则找到泄露点,修正;如果确实是合理的存在,那么就增加堆空间。(内存分析这里我也木有做过,工具也木有使用过,在后续章节会有介绍,用熟了后再来一篇)

2.2 虚拟机栈和本地方法栈溢出

     由于在HotSpot虚拟机中并不区分虚拟机栈和本地方法区栈,因此对于HotSpot来说,-Xoss(设置本地方法栈大小)参数是无效的,栈容量由-Xss参数设定。关于虚拟机栈和本地方法区栈,在Java虚拟机规范中描述了两种异常:

     如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出*Error异常
如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常
书中谈到单线程的场景下只能浮现*Error,那我们就先来看看单线程场景下到底会是什么样子。

/** * * VM Args:-Xss128k */
public class JavaVMStackSOF {
  private int stackLength = 1;
  private void stackLeak() {
    stackLength++;
    stackLeak();
  }
  public static void main(String[] args) throws Throwable {
    JavaVMStackSOF oom = new JavaVMStackSOF();
    try {
      oom.stackLeak();
    } catch (Throwable e) {
      System.out.println("stack length:" + oom.stackLength);
      throw e;
    }
  }
}

通过-Xss128k设置虚拟机栈大小为128k,执行结果如下:

执行结果显示,确实是发生了*Error异常。

     通过不断创建线程耗尽内存也可以呈现出OutOfMemoryError异常,但是在Windows平台下模拟会使系统死机,我这里就不多说了。感兴趣的可以自己去尝试。

2.3 运行时常量池溢出

     向运行时常量池中添加内容最简单的方式就是使用String.intern()方法。由于常量池分配在方法区内,可以通过-XX:PermSize和-XX:MaxPermSize限制方法区的大小,从而间接限制其中常量池的容量。

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

  /** * VM Args:-XX:PermSize=10M -XX:MaxPermSize=10M * */
  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());
      }
    }

  }

     这里有个小小的插曲,之前有听说在jdk7中将永久区(方法区和常量池)给干掉了,没有验证过。永久区可以说是在堆之上的一个逻辑分区。如果jdk7中去掉了,那么这个示例应该会抛出堆空间的内存溢出,而非运行时常量池的内存溢出。所以在执行程序的时候分别用了jdk6和jdk7两个版本。多说一句,如果jdk7去掉了方法区,那么-XX:PermSize=10M -XX:MaxPermSize=10M就不起作用了,所以在jdk7环境下运行时,堆大小为jvm默认的大小,要执行一会儿(半小时左右:( ))才能抛出异常。没关系,再配置下运行时参数即可,注意要配置成不可扩展。以图为据:

jdk6环境下抛出运行时常量池内存溢出
Exception in thread “main” java.lang.OutOfMemoryError: PermGen space
显而易见PermGen space,永久区。不解释。

jdk7环境下,运行时参数为:-XX:PermSize=10M -XX:MaxPermSize=10M

Exception in thread “main” java.lang.OutOfMemoryError: Java heap space
运行了好久好久,最终抛出堆内存溢出。Java heap space已经足够说明问题了。

jdk7环境下,运行时参数为:-verbose:gc -Xms20M -Xmx20M -XX:+HeapDumpOnOutOfMemoryError

Exception in thread “main” java.lang.OutOfMemoryError: Java heap space
同样也是堆内存溢出,不过速度就快了好多好多,因为堆大小被设置为不可扩展。

2.4 方法区溢出

     方法区用于存放Class的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。测试这个区域只要在运行时产生大量的类填满方法区,知道溢出。书中借助CGlib直接操作字节码运行时,生成了大量的动态类。

     当前主流的Spring和Hibernate对类进行增强时,都会使用到CGLib这类字节码技术,增强的类越多,就需要越大的方法区来保证动态生成的Class可以加载到内存。

测试代码如下:

import net.sf.cglib.proxy.Enhancer;
  import net.sf.cglib.proxy.MethodInterceptor;
  import net.sf.cglib.proxy.MethodProxy;

  /** * 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() {
          public Object intercept(Object object, Method method,
              Object[] args, MethodProxy proxy) throws Throwable{
            return proxy.invokeSuper(object, args);
          }
        });
        enhancer.create();
      }

    }

    static class OOMObject {

    }

  }

列表内容

工程中要引入cglib-2.2.2.jar和asm-all-3.3.jar。

方法区的内存溢出问题同样存在jdk6和jdk7版本之间的区别,同运行时常量池内存溢出。

     方法区溢出也是一种常见的内存溢出异常,一个类如果要被垃圾收集器回收掉,判定条件是非常苛刻的。在经常动态生成大量Class的应用中,需要特别注意类的回收状况。这类场景除了上面提到的程序使用了CGLib字节码增强外,常见的还有:大量JSP或动态生成JSP文件的应用、基于OSGi的应用等。

2.5 本机直接内存溢出

DirectMemory容量可以通过-XX:MaxDirectMemorySize指定。

示例代码如下:

import java.lang.reflect.Field;
  import sun.misc.Unsafe;

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

    private static final int _1MB = 1024 * 1024;

    /** * @param args * @throws IllegalAccessException * @throws IllegalArgumentException */
    public static void main(String[] args) throws IllegalArgumentException,
        IllegalAccessException {
      // TODO Auto-generated method stub
      Field unsafeField = Unsafe.class.getDeclaredFields()[0];
      unsafeField.setAccessible(true);
      Unsafe unsafe = (Unsafe) unsafeField.get(null);

      while(true){
        unsafe.allocateMemory(_1MB);
      }
    }

  }

运行结果如下图: