手记

死磕JVM-如何构造JVM内存溢出和栈溢出

为什么要写这个题目?我记得我在面试阿里的时候面试官问了我这个问题,当时没能答得很好,只说了些概念的东西,很是心虚,于是下定决心要把这个问题搞懂,现在终于把这个问题怼清楚了,分享给大家,希望你们以后面试问到这种问题能有所准备。

Java虚拟机中描述了两种异常:

1、如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常;

2、如果在虚拟机中无法申请到足够多的内存空间,将抛出OutOfMemoryError异常。

我们都知道Java虚拟机各个内存区域(除了程序计数器)都有发生内存溢出的可能,但到底什么样的操作或程序才会导致内存溢出或栈溢出的异常呢?我们分不同的内存区域来解释这个问题。

一、对于Java堆内存区域

Java堆中只会产生OutOfMemoryError异常。

先搞清楚Java堆内存放的是什么,还不清楚的可以回顾下这篇文章《死磕JVM-Java内存模型》,从这篇文章里我们知道Java堆内存存放的是对象实例,所以原理上只要我们不断创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清楚这些对象,也就是说当Eden区满的时候,GC被触发时,让GC误以为内存中的对象还存活着,那么在对象数量达到最大堆容量限制的时候就会产生内存溢出的异常。如下代码就会产生内存溢出的异常:

public class 堆溢出 {

     static class OOMError{}

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

运行结果:

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:261)
     at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:235)
     at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:227)
     at java.util.ArrayList.add(ArrayList.java:458)
     at com.intelligentler.jvm.堆溢出.main(堆溢出.java:13)

“Java heap space”提示着产生OutOfMemoryError异常的Java虚拟机的内存区域,也就是Java堆内存。

如何解决发生在Java堆内存的OutOfMemoryError异常呢?

首先我们要分清楚产生OutOfMemoryError异常的原因是内存泄露还是内存溢出,如果内存中的对象确实都必须存活着而不像上面那样不断地创建对象实例却不使用该对象,则是内存溢出,而像上面代码中的情况则是内存泄露。

如果是内存泄露,我们可以通过一些内存查看工具来查看泄露对象到GC Roots的引用链,找到泄露对象是通过怎样的路径与GC Roots相关联并导致GC无法自动回收这些泄露对象,掌握了这些信息,我们就能比较准确地定位出泄露代码的位置。

如果不是内存泄露,也就是说内存中的对象确实都还必须存活,那么应该检查虚拟机的堆参数,看看是否还可以将机器物理内存调大,同时在代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况。

二、对于虚拟机栈和本地方法栈

在这一部分内存区域,可能产生OutOfMemoryError异常和StackOverflowError异常。

如果定义大量的本地变量,增大此方法帧中本地变量表的长度或者设置-Xss参数减少栈内存容量,这两种操作都会抛出StackOverflowError异常,如下面的代码:

public class 栈溢出 {

     private int stackLength = 1;

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

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

}

运行结果:

stack length:18388
Exception in thread "main" java.lang.StackOverflowError
     at com.intelligentler.jvm.栈溢出.addStackLength(栈溢出.java:9)
     at com.intelligentler.jvm.栈溢出.addStackLength(栈溢出.java:9)
     at com.intelligentler.jvm.栈溢出.addStackLength(栈溢出.java:9)

所以,如果在单线程的情况下,无论是栈帧太大还是虚拟机栈容量太小,当内存无法再分配的时候,虚拟机抛出的是StackOverflowError异常。

如果在多线程下,不断地建立线程可能会产生OutOfMemoryError异常。

三、对于方法区

方法区中只会产生OutOfMemoryError异常。

由于运行时常量池是方法区的一部分,我们可以通过String.intern()方法来构建一个运行时常量池的OutOfMemoryError异常。

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

public class 方法区溢出 {

     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)

PermGen space的全称是Permanent Generation space,是指内存的永久保存区域,也就是说运行时常量池属于方法区(也就是虚拟机永久代)中的一部分。

另外,方法区是存放Class的相关信息的,运行时如果有大量的类来填满方法区,就会产生OutOfMemoryError异常。

2人推荐
随时随地看视频
慕课网APP