Java自学者论坛

 找回密码
 立即注册

手机号码,快捷登录

恭喜Java自学者论坛(https://www.javazxz.com)已经为数万Java学习者服务超过8年了!积累会员资料超过10000G+
成为本站VIP会员,下载本站10000G+会员资源,会员资料板块,购买链接:点击进入购买VIP会员

JAVA高级面试进阶训练营视频教程

Java架构师系统进阶VIP课程

分布式高可用全栈开发微服务教程Go语言视频零基础入门到精通Java架构师3期(课件+源码)
Java开发全终端实战租房项目视频教程SpringBoot2.X入门到高级使用教程大数据培训第六期全套视频教程深度学习(CNN RNN GAN)算法原理Java亿级流量电商系统视频教程
互联网架构师视频教程年薪50万Spark2.0从入门到精通年薪50万!人工智能学习路线教程年薪50万大数据入门到精通学习路线年薪50万机器学习入门到精通教程
仿小米商城类app和小程序视频教程深度学习数据分析基础到实战最新黑马javaEE2.1就业课程从 0到JVM实战高手教程MySQL入门到精通教程
查看: 12117|回复: 0

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

[复制链接]
  • TA的每日心情
    奋斗
    2024-11-24 15:47
  • 签到天数: 804 天

    [LV.10]以坛为家III

    2053

    主题

    2111

    帖子

    72万

    积分

    管理员

    Rank: 9Rank: 9Rank: 9

    积分
    726782
    发表于 2021-4-11 12:49:38 | 显示全部楼层 |阅读模式

     

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

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

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

    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 }
    View Code

     

    jvm虚拟机启动参数设置:

     

    运行结果:

    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)
    View Code

     

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

     

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

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

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

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

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

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

     1 package Text.JVM;
     2 
     3 
     4 /*
     5  * 线程请求的栈深度超过虚拟机所允许的最大深度,将抛出StackOverflowError异常
     6  * 最常见引起此类异常的情形时使用不合理的递归调用
     7  * VM Args:-Xss256k
     8  * 
     9  * @author Administrator
    10  * 
    11  */
    12 public class StackOverflowError {
    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         StackOverflowError oomError = new StackOverflowError();
    26         try {
    27             oomError.stackLeak();
    28         } catch (Throwable e) {
    29             System.out.println("栈深度为:" + oomError.stackLength);
    30             throw e;
    31         }
    32     }
    33 
    34 }
    View Code

     

    程序运行结果:

    栈深度为:2491
    Exception in thread "main" java.lang.StackOverflowError
        at Text.JVM.StackOverflowError.stackLeak(StackOverflowError.java:19)
        at Text.JVM.StackOverflowError.stackLeak(StackOverflowError.java:20)
        at Text.JVM.StackOverflowError.stackLeak(StackOverflowError.java:20)
        at Text.JVM.StackOverflowError.stackLeak(StackOverflowError.java:20)
      ......
    View Code

     

    异常信息中的:java.lang.StackOverflowError表明了内存溢出区域为虚拟机栈。

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

     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 }
    View Code

     

    理论上本段代码的运行结果应该是: 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 }
    View Code

     

    据说此段代码在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 }
    View Code

    运行结果:

    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 
    View Code

     

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

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

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

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

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

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

    哎...今天够累的,签到来了1...
    回复

    使用道具 举报

    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    QQ|手机版|小黑屋|Java自学者论坛 ( 声明:本站文章及资料整理自互联网,用于Java自学者交流学习使用,对资料版权不负任何法律责任,若有侵权请及时联系客服屏蔽删除 )

    GMT+8, 2024-12-22 23:59 , Processed in 0.062279 second(s), 29 queries .

    Powered by Discuz! X3.4

    Copyright © 2001-2021, Tencent Cloud.

    快速回复 返回顶部 返回列表