如何写出让java虚拟机发生内存溢出异常OutOfMemoryError的代码

时间:2022-01-18 07:23:17

程序小白在写代码的过程中,经常会不经意间写出发生内存溢出异常的代码。很多时候这类异常如何产生的都傻傻弄不清楚,如果能故意写出让jvm发生内存溢出的代码,有时候看来也并非一件容易的事。最近通过学习《深入理解java虚拟机-JVM高级特性与最佳实践》这本书,终于初步了解了一下java虚拟机的内存模型。本文通过写出使jvm发生内存溢出异常的代码来对自己的学习结果进行总结,同时也提醒自己以后写代码时候不要再跳进这个坑啦。

java的内存管理是由java虚拟机自动进行管理的,并不需要程序员过多的手动干预,这也就导致了初学java的人在不了解java内存模型的情况下也能愉快的进行coding。不过一旦涉及了内存泄露或者内存溢出以及垃圾回收(GC)方面的问题,如果不了解虚拟机是怎么管理内存的,那么排查问题,定位错误地点就显得无从下手了。首先上图看一下java虚拟机运行时数据区是什么样子(图片来源于网络):

如何写出让java虚拟机发生内存溢出异常OutOfMemoryError的代码

从上图我们可以获得以下信息:

  1. jvm内存分区可以分为所有线程共享数据区以及线程隔离数据区两部分。这也就是说图中的方法区和堆是由所有的线程共同使用的,而虚拟机栈、本地方法栈、程序计数器则是每个线程独有各自的响应数据区,各线程之间是互不干扰的。
  2. jvm运行时数据区按照功能可分为方法区,堆,虚拟机栈,本地方法栈,程序计数器五个部分。各个部分存储的数据类型不同。

本文主要讲述如何让JVM发生内存溢出异常,有关JVM内存模型将会在另一篇文章中详细讲解,这里简单介绍各分区的作用:

  1. 堆:各线程共享;存放java对象实例,以及为数组分配的空间也在此处
  2. 虚拟机栈:线程私有;生命周期与线程相同,描述java方法执行的内存模型,每个方法在执行时创建栈帧,栈帧存储局部变量表、操作数栈、动态链接、返回地址(方法出口)等信息
  3. 本地方法栈:和虚拟机栈功能类似,线程私有;为虚拟机使用的Native方法提供服务
  4. 方法区:各线程共享;存储已被虚拟机加载的类的信息、final声明的常量,static修饰的静态变量,以及编译器编译后的java代码等数据(jdk7以后把常量池移到了堆中)
  5. 程序计数器:线程私有;可以看做是当前线程所执行程序的字节码的行号指示器

在jvm规范中,除了程序计数器内存区域没有规定任何内存溢出异常情形外,其他四个内存区域都会有相应的内存溢出异常发生的可能,所以jvm内存溢出异常发生在不同的内存区域具有不同的异常发生原因,知道一内存异常产生的位置,对于定位错误地点就很有指向性了。

下面就通过实例来展示,如何通过代码指定让不同的内存区域发生内存溢出异常。

一、java堆发生内存溢出:

java堆是用来存储对象实例以及数组的,使java堆发生内存溢出的要旨是:

  • 不断创建对象
  • 保证对象存活,不会被垃圾收集器回收

java虚拟机的内存大小是可以人为设置的,通过设置限制内存大小,可以很方便的实现内存溢出,节约了时间。

设置java堆内存大小的虚拟机参数为:-Xms堆的初始大小 -Xmx堆可扩展的最大值

 1 package Text.JVM;
2
3 import java.util.ArrayList;
4 import java.util.List;
5
6 /**
7 * JVM堆中存储java对象实例和数组
8 * VM Args: -Xms20m -Xmx20m (限制堆的大小不可扩展)
9 * @author Administrator
10 *
11 */
12 public class HeapOutOfMemoryError {
13
14 public static class OOMObject {
15 }
16 public static void main(String[] args) {
17 List<Object> list=new ArrayList<>();
18 // 不断创建对象,并保证GC Roots到对象之间有可达路径,避免垃圾回收清除创建的对象
19 while (true) {
20 list.add(new OOMObject());
21 System.out.println(System.currentTimeMillis());
22 }
23 }
24
25 }

jvm虚拟机启动参数设置:

如何写出让java虚拟机发生内存溢出异常OutOfMemoryError的代码

运行结果:

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:2245)
at java.util.Arrays.copyOf(Arrays.java:2219)
at java.util.ArrayList.grow(ArrayList.java:242)
at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:216)
at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:208)
at java.util.ArrayList.add(ArrayList.java:440)
at Text.JVM.HeapOutOfMemoryError.main(HeapOutOfMemoryError.java:20)

注意运行结果第一行末尾:java.lang.OutOfMemoryError: Java heap space 。java heap space明确的指出了异常发生的区域:堆。

二、虚拟机栈发生内存溢出异常:

能够使虚拟机栈发生内存溢出异常的情形有两种:

  1. 线程请求的栈深度超过虚拟机所允许的最大深度,将抛出*Error异常。
  2. 虚拟机在扩展栈时无法申请到足够的内存空间,将抛出OutOfMemoryError异常。

使java虚拟机栈发生内存溢出异常的要旨:

  • 对应情形1,使用不合理的递归
  • 对应情形2,不断创建活跃的线程

使用递归导致虚拟机栈内存溢出异常的示例:

 1 package Text.JVM;
2
3
4 /*
5 * 线程请求的栈深度超过虚拟机所允许的最大深度,将抛出*Error异常
6 * 最常见引起此类异常的情形时使用不合理的递归调用
7 * VM Args:-Xss256k
8 *
9 * @author Administrator
10 *
11 */
12 public class *Error {
13
14 // 记录内存溢出时的栈深度
15 private int stackLength = 1;
16
17 // 递归调用的方法
18 public void stackLeak() {
19 stackLength++;
20 stackLeak();
21 }
22
23 public static void main(String[] args) {
24
25 *Error oomError = new *Error();
26 try {
27 oomError.stackLeak();
28 } catch (Throwable e) {
29 System.out.println("栈深度为:" + oomError.stackLength);
30 throw e;
31 }
32 }
33
34 }

程序运行结果:

栈深度为:2491
Exception in thread "main" java.lang.*Error
at Text.JVM.*Error.stackLeak(*Error.java:19)
at Text.JVM.*Error.stackLeak(*Error.java:20)
at Text.JVM.*Error.stackLeak(*Error.java:20)
at Text.JVM.*Error.stackLeak(*Error.java:20)
  ......
异常信息中的:java.lang.*Error表明了内存溢出区域为虚拟机栈。

通过创建线程导致虚拟机栈内存溢出异常的示例:

 1 package Text.JVM;
2
3 /*
4 * 通过不断创建活跃线程,消耗虚拟机栈资源
5 * VM Args:-Xss256k
6 */
7 public class StackOutOfMemoryError {
8
9 // 线程任务,每个线程任务一直在运行
10 private void wontStop() {
11 while (true) {
12 System.out.println(System.currentTimeMillis());
13 }
14 }
15
16 // 不断地创建线程
17 public void stackLeadByThread() {
18 while (true) {
19 Thread thread = new Thread(new Runnable() {
20
21 @Override
22 public void run() {
23 wontStop();
24 }
25 });
26 thread.start();
27 }
28 }
29
30 public static void main(String[] args) {
31 StackOutOfMemoryError oomError=new StackOutOfMemoryError();
32 oomError.stackLeadByThread();
33 }
34
35 }

理论上本段代码的运行结果应该是: Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread ,但是由于windows平台虚拟机java线程映射到操作系统的内核线程上,执行上述代码有较大风险可能会导致操作系统假死(我运行完是真的死了),所以运行需谨慎!!!(写了这么长的博客有一半没保存,重启后丢了这我会乱说???都是泪啊)。

三、方法区和运行时常量池内存溢出异常

方法区的作用是存储 Java 类的结构信息,当我们创建对象实例后,对象的类型信息存储在方法区之中,实例数据存放在堆中;实例数据指的是在 Java 中创建的各种实例对象以及它们的值,类型信息指的是定义在 Java 代码中的常量、静态变量、以及在类中声明的各种方法、方法字段等等;同时可能包括即时编译器编译后产生的代码数据。通过在运行时产生大量的类,或者工程本身具有大量的类,而方法区分配的空间不足以容纳如此多的类信息的时候就会产生方法区内存溢出异常。

运行时常量池是方法取得一部分,程序中使用到的String类型字面量以及基本数据类型的一部分数据会存储在常量池中。我们使用String.intern()方法来测试,是运行时常量池发生内存溢出。此方法的作用是:如果字符串常量池中不包含一个等于此String对象的字符串,则将此对象包含的字符串添加到常量池中,并返回此对象的引用。

使方法区发生内存溢出的要旨:

  • 程序运行时动态创建的大量类,导致方法区内存空间不足
  • 程序中存有大量字面量等数据导致常量区内存不足

常量池内存溢出示例代码(仅在jdk6之前的版本中有效):

1 package Text.JVM;
2
3 import java.util.ArrayList;
4 import java.util.List;
5
6 /*
7 * VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M(限制常量池容量)
8 *
9 */
10 public class RunTimeConstantPoolOutOfMemoryError {
11
12 public static void main(String[] args) {
13 // 使用list保持常量池引用,避免常量池内的数据被垃圾回收清除
14 List<String> list = new ArrayList<>();
15 long i = 0;
16 while (true) {
17 String string = (i++) + "";
18 list.add(string.intern());
19 }
20 }
21
22 }

据说此段代码在jdk6之前的版本中运行时会产生:Exception in thread "main" java.lang.OutOfMemoryError: PermGen space 其中PermGen space指示内存溢出发生在运行时常量池中。

但是,我在jdk7的环境中运行得到的结果却是: Exception in thread "main" java.lang.OutOfMemoryError: Java heap space  指示内存溢出发生在堆中而不是方法区中的常量池!!!都说实践是检验真理的唯一标准,果真是没错的。因为在 JDK1.2 ~ JDK6 的实现中,HotSpot 使用永久代实现方法区,而从 JDK7 开始 Oracle HotSpot 开始移除永久代,JDK7中符号表被移动到 Native Heap中,字符串常量和类引用被移动到 Java Heap中。在 JDK8 中,永久代已完全被元空间(Meatspace)所取代。关于常量池的存放位置还有待进一步研究,不过上段代码是可以引起常量池的内存溢出的。

通过运行时动态产生大量的类产生方法区内存溢出示例这里就不提供了,书中提供了使用CGLib使方法区出现内存异常的示例:

 1 /**
2 * VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M
3 * @author zzm
4 */
5 public class JavaMethodAreaOOM {
6
7 public static void main(String[] args) {
8 while (true) {
9 Enhancer enhancer = new Enhancer();
10 enhancer.setSuperclass(OOMObject.class);
11 enhancer.setUseCache(false);
12 enhancer.setCallback(new MethodInterceptor() {
13 public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
14 return proxy.invokeSuper(obj, args);
15 }
16 });
17 enhancer.create();
18 }
19 }
20
21 static class OOMObject {
22
23 }
24 }

运行结果:

Caused by: java.lang.OutOfMemoryError: PermGen space
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClassCond(ClassLoader.java:632)
at java.lang.ClassLoader.defineClass(ClassLoader.java:616)
... 8 more

我并没有运行过这段代码,因为我本人对于CGLib并不了解,这里只要知道发生内存溢出时显示:PermGen space那就是方法区内存溢出准没错啦。

四、直接内存不足导致内存溢出异常

直接内存不是虚拟机运行时的数据区的一部分,也不是在java虚拟机规范中定义的区域,但是这部分区域也被频繁使用,也会导致OutOfMemoryError

直接内存应用于NIO,直接内存区域默认为对内存的最大值,通过-XX:MaxDirectMemorySize可以显式的指定直接内存大小,如果忽略直接内存,容易使各个内存区域总和大于物理内存限制,从而导致动态扩展时出现内存溢出现象。

具体的代码示例也就不贴了,因为平时的学习过程中还没有使用过或者还没有意识到自己使用过直接内存区。

(注:以上内容完全是为了记录学习结果,所有内容皆为原创,如果觉得对您有用欢迎转载,但请注明出处,尊重原创,如果文内有内容不对的地方还请多多指教)