如何构造jvm的堆溢出和栈溢出

时间:2022-07-25
本文章向大家介绍如何构造jvm的堆溢出和栈溢出,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

构造堆溢出和栈溢出

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

  • 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常;—-栈溢出
  • 如果在虚拟机中无法申请到足够多的内存空间,将抛出OutOfMemoryError异常。—-堆溢出

堆溢出

在java堆中只会产生OutOfMemoryError异常

首先,我们知道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());
          }
     }
}

虽然这里产生了堆溢出,但是我们需要注意产生这个异常的原因是内存溢出还是内存泄露

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

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

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

栈溢出

虚拟机栈用于存储局部变量表、操作数栈、常量池引用等信息。

所以想让栈溢出,我们只需要定义大量的局部变量,增大此方法帧中本地变量表的长度或者设置-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;
          }
     }
 
} 

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

·在多线程下,不断地建立线程可能会产生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());
          }
     }
 
}

当然,还可以添加大量的类,比如一些框架大量使用反射,如果不具备卸载类的方法,将很快占满方法区