文章目录

  • 一、栈溢出
  • 二、堆溢出
  • 三、运行时常量池溢出
  • 四、方法区溢出
  • 五、直接内存溢出
  • 六、Java异常体系


在Java内存区域中介绍了JVM的内存区域,其中除了程序计数器外,虚拟机内存的其他几个运行时区域都有发生OutOfMemoryError异常的可能。


一、栈溢出

每个Java方法在被调用的时候,都会创建一个栈帧并入栈,那么这里我们直接无限调用递归方法,即可让虚拟机栈溢出。

public class StackSOF {

    private int stackLength = 1;

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

    public static void main(String[] args) {
        StackSOF stackSOF = new StackSOF();
        try {
            stackSOF.stackLeak();
        } catch (Throwable e) {
            System.out.println(stackSOF.stackLength);
            e.printStackTrace();
        }
    }
}

java栈溢出伪代码 java栈溢出异常_方法区

上述截图中控制台中打印了很多次异常,这是因为我们一共调用了61702次方法,每个方法都会抛出一个异常,栈中存储的栈帧数量也是受到栈帧大小的影响的,如我们直接在方法中随机加入一些参数,我们再看看栈的深度。

public class StackSOF {

    private int stackLength = 1;

    public void stackLeak(String arg1, String arg2) {
        stackLength++;
        stackLeak(arg1, arg2);
    }

    public static void main(String[] args) {
        StackSOF stackSOF = new StackSOF();
        try {
            stackSOF.stackLeak("123456789123456789", "abcdefgabcdefgabcdefgabcdefg");
        } catch (Throwable e) {
            System.out.println(stackSOF.stackLength);
            e.printStackTrace();
        }
    }
}

java栈溢出伪代码 java栈溢出异常_方法区_02

注意: 上述结果中显示的都是java.lang.StackOverflowError,其实在栈区也是可能发生java.lang.OutOfMemoryError异常的,因为JVM只限制单个虚拟机栈的大小,栈区的空间是没有办法去限制的,因为在运行过程中会有线程不断的运行,所以没办法限制。这里只需不断建立线程,JVM申请栈内存,待机器没有足够的内存,栈区就会发生java.lang.OutOfMemoryError异常。



二、堆溢出

堆空间一般是程序启动时就申请了,那么只需限制其大小,然后再申请超出最大堆内存空间即可,如下:

public class HeapOOM {
    public static void main(String[] args) {
        String[] arr = new String[6*1024*1024];    // 6m
    }
}

在运行上述代码之前,我们一定要记得先设置虚拟机的启动参数,限制其堆空间大小为5m,如下

java栈溢出伪代码 java栈溢出异常_方法区_03


java栈溢出伪代码 java栈溢出异常_常量池_04



另外堆是JVM上最大的内存区域,我们申请的几乎所有的对象,都是在这里存储的,如果我们不断地创建对象,并且保证GC Roots到对象的之间有可达路径来避免垃圾回收机制清除这些对象,那么在对象数量到达最大堆的容量限制后也会产生内存溢出的异常。

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

在运行上述代码之前,可以添加虚拟机的参数-XX:+PrintGC或者-XX:+PrintGCDetails来打印GC的日志,这里我们直接向GC日志打印至工作台,也可以通过参数-Xloggc:filename将其打印至我们指定目录的文件之中

java栈溢出伪代码 java栈溢出异常_方法区_05


java栈溢出伪代码 java栈溢出异常_方法区_06


我们会发现在发生了很多次的GC回收之后,会抛出java.lang.OutOfMemoryError: GC overhead limit exceeded异常,对于这个异常,官方的解释是:

超过98%的时间用来做GC并且回收了不到2%的堆内存时会抛出此异常。

JVM给出这样一个参数:-XX:-UseGCOverheadLimit禁用这个检查,其实这个参数解决不了内存问题,只是把错误的信息延后,替换成java.lang.OutOfMemoryError: Java heap space

java栈溢出伪代码 java栈溢出异常_Java_07


java栈溢出伪代码 java栈溢出异常_方法区_08



三、运行时常量池溢出

JVM内存区域中的运行时常量池,在不同jdk版本之中都是不同的,在JDK1.6之前,我们的运行时常量池是包含在方法区之中的;在JDK1.7之后,我们的运行时常量池从方法区中移动到了Java堆之中。

所以我们的不同的JDK版本志宏运行时常量其溢出,可能会出现不同的结果,如在JDK1.6及其之前,运行时常量池溢出会导致方法区内存溢出,而在jdk1.7及其之后,运行时常量池溢出会导致堆的内存溢出。

这里我们可以使用String.intern()方法,它的作用:如果字符串常量池中已经包含一个等于此String对象的字符串,则放回代表池中的这个字符串的String对象;否则,将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。

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

这里使用的时JDK1.8版本,所以再运行前还需要限定一下堆的大小

java栈溢出伪代码 java栈溢出异常_常量池_09


java栈溢出伪代码 java栈溢出异常_常量池_10



四、方法区溢出

方法区在JDK的不同版本之中有不同的定义及参数设值,如在JDK1.6之前,我们的方法区中还包含了运行时常量池,这里我们就可以直接向运行时常量池中添加大量的数据,使其溢出,从而我们的方法区也会溢出。

由于我们使用的JDK1.8,所以上述我们演示运行时常量池溢出导致的是Java堆的溢出。既然我们不能借助运行时常量池溢出导致方法区内存溢出,那我们还可以使用CGLIB来无限的代理生成增强类,使其方法区溢出。

public class MethodAreaOOM {
    public static void main(String[] args) {
        while (true) {
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(Target.class);
            enhancer.setCallback(new CglibProxy());
            enhancer.setUseCache(false);
            enhancer.create();
        }
    }

    static class Target{

    }

    static class CglibProxy implements MethodInterceptor {
        @Override
        public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
            return methodProxy.invokeSuper(o,objects);
        }
    }
}

使用Cglib需要引入相应的jar包,依赖如下,也可以下载相应的 .jar 文件导入项目中

<!-- https://mvnrepository.com/artifact/cglib/cglib -->
<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>3.2.5</version>
</dependency>

另外在JDK8之后,方法区大小就只受本机总内存的限制,测试前需要先设定一下方法区的大小

java栈溢出伪代码 java栈溢出异常_java栈溢出伪代码_11


java栈溢出伪代码 java栈溢出异常_Java_12



五、直接内存溢出

直接内存一般在网络通信NIO中使用较多,在我们的NIO中为我们提供了可以直接分配直接内存的方法,如下

import java.nio.ByteBuffer;

public class DirectMemoryOOM {
    public static void main(String[] args) {
        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(12*1024*1024);
    }
}

java栈溢出伪代码 java栈溢出异常_java_13


java栈溢出伪代码 java栈溢出异常_Java_14



六、Java异常体系

最后补充一点,上述在测试栈溢出中使用了try-catch,在catch代码块中进行了打印处理,注意我们选择catch (Throwable),而不是catch (Exception),因为我们内存溢出抛出的是Error了,而不是Exception

java栈溢出伪代码 java栈溢出异常_Java_15

这里我们简单了解一下Java的异常体系:

  • Throwable: Java中所有异常和错误类的父类。只有这个类的实例(或者子类的实例)可以被虚拟机抛出或者被java的throw关键字抛出。同样,只有其或其子类可以出现在catch子句里面。
  • Error: Throwable的子类,表示严重的问题发生了,而且这种错误是不可恢复的。
  • Exception: Throwable的子类,应用程序应该要捕获其或其子类(RuntimeException例外),称为checked exception。比如:IOException, NoSuchMethodException…
  • RuntimeException: Exception的子类,运行时异常,程序可以不捕获,称为unchecked exception。比如:NullPointException。