异常处理机制】 Java出现OutOf MemoryError(OOM 错误)的原因有哪些?出现OOM错误后,怎么解

2021-11-09 09:27发布

5条回答
我是大脸猫
2021-11-09 10:36

在Java虚拟机的运行时数据区域,除了程序计数器之外,都可能会出现outOfMemoryError异常。当我们遇到实际的内存溢出异常时,首先要能根据异常异常的信息快速判断哪个区域的内存溢出,知道什么样的代码可能会导致内存溢出,以及出现异常之后该如何处理。


     操作系统为每个进程分配的内存是具有一定限制性。譬如,32位的操作系统限制为2GB。而当检查出哪个区域出现OutOfMemoryError异常时,需要平衡该进程的某个区域内存大小来解决另一个区域出现内存溢出的情况。例如,在高并发情况下,不断地创建线程可能会使线程私有的虚拟机栈出现内存溢出情况,在不能减少线程数或者更换更大位数的操作系统时,就只能通过减少最大堆和减少栈容量的方式来解决虚拟机栈导致的内存溢出。所以,无论在处理异常或者是实验,我们都要学会调整设置虚拟机启动参数(设置虚拟机启动参数(eclipse).note)。


一、Java堆溢出原因及其解决方案

     Java堆用于存储对象实例和数组的,只要不断地创建对象或者数组,并且保证GC Roots到对象之间有可达的路径来避免垃圾回收机制清除这些对象,那么在对象数量达到最大堆的容量限制后就会产生内存溢出异常。要了解Java堆内存可能溢出原因,首先我们要知道当生成新对象时,向Java堆申请内存的过程:


     ① JVM先尝试在Eden区(Young区包括1个Eden和2个survivor)分配新建对象所需要的内存;


     ② 如果内存大小足够,则申请结束。反之,执行下一步;


     ③ JVM启动新生代GC,试图将Eden区中不活跃的对象释放掉,释放后若Eden空间仍然不足以放入新对象,则试图将部分Eden中活跃对象放入Survivor区;


     ④ Survivor区被用来作为Eden及old的中间交换区域,当old区空间足够时,Survivor区的对象会被移到old区,否则会被保留在Survivor区;


     ⑤ 当old区空间不够时,JVM会在old区进行GC;


     ⑥ old区被清理后,若old区仍无法存放从Eden复制过来的部分对象,导致JVM无法在Eden区为新生对象创建内存区域,则会出现内存溢出异常(OutOfMemmory)。


例:当对象无限被创建且无法被回收时,Java堆区将会出现内存溢出异常,如下




解决方案:


     Java堆出现内存溢出异常有两种解决方案。一种是基于内存调整来改变堆区内存大小以便能够存储更多的对象,但堆内存受到物理内存的限制,当出现无法再扩展堆内存的情况时,就采用第二种方式,从代码上检查是否存在某些对象的生命周期过长、持有状态时间过长的情况,尝试减少程序在运行期的内存消耗。


二、虚拟机栈和本地方法栈溢出

     虚拟机栈和本地方法栈的内存分布在不同的虚拟机上是有差别的。例如,在HotSpot上是将虚拟机栈和本地方法栈合二为一的。虽然有-Xoss参数用于设置本地方法栈,但实际上是无效的,栈容量只由-Xss参数设定。关于虚拟机栈和本地方法栈,在Java虚拟机规范中描述了两种异常:①如果线程请求的栈深度大于虚拟机所允许的最大栈深度,将抛出StackOverflowError异常。②如果虚拟机在扩展栈时无法申请到足够的空间,则抛出OutOfMemoryError异常。


     导致这两种异常的原因是有一定区别的。虚拟机栈是线程私有的,它存储的是存放着局部变量表、操作数栈、动态链接以及方法出口等信息的栈帧,当前线程每执行一个方法时,实际上都对应着一个栈帧在虚拟机栈中出栈到入栈的过程,而当这个线程执行方法较多时,创建的栈帧也会随之越来越多,而虚拟机栈内存是受到了JVM总体内存的一个分布限制,一旦栈帧较多时,可能就会出现栈满溢出异常,也就是StackOverflowError异常。而出现OutOfMemoryError异常的情况是相对于多线程环境下而言的,一旦线程数量过多,并且都没有即使回收,从而会不断地申请内存给虚拟机栈,从而导致在扩展栈时无法申请到足够的空间,出现OutOfMemoryError异常。当然,OutOfMemoryError异常也可能出现在单钱程的情况下。当为一个线程设置虚拟机栈内存大小与其它区内存之和大于JVM所允许的最大内存,就可能出现OutOfMemoryError异常。


设置虚拟机栈内存大小为 -Xss128k,使其出现StackOverflowError异常。测试程序如下:




解决方案:


由以上可看出,StackLeak()的递归调用使该线程不断地去创建一个栈帧存入该线程的虚拟机栈中,最终导致栈满溢出出现StackOverflowError异常。解决StackOverflowError异常的方法同样有从内存大小和Java程序两个方面来解决问题。从内存大小上,增加每个线程的虚拟机栈大小,这种方式虽然简便,但在多线程的情况下,随着每个线程所属的虚拟机栈的大小的增加,所能执行的线程也就随之减少了,但我们使用这种方式来解决StackOverflowError异常时,通常要权衡这两个反比因素。从Java程序上无疑就是检查是否出现方法的不合理递归调用,从而减少栈帧的创建和及时的回收。


设置虚拟机栈大小为2M时,会出现OutOfMemoryError异常。测试程序如下:




注:Java线程执行到系统的内核线程上,有较大的风险。比如我的死机了......


解决方案:


     和StackOverflowError一样,通过改变内存大小和Java程序来解决这个问题。一般我们会优先考虑从Java程序上来进行改善,例如给线程一个固定的死亡时间,使其不会出现大量线程不能被回收的情况,或者另一方面减少线程数。而当我们无法减少线程数时和无法更换64位虚拟机的情况下,我们只能通过减少堆内存和栈容量来权衡这一部分的内存消耗。


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

     在jdk1.7之前,运行时常量池属于方法区的一部分,也就是运行时常量池处于永久代。而方法区出现的内存溢出异常经常是由运行时常量池内存溢出异常所导致的。但在jdk1.7时,为了消除永久代,常量池位置发生了一定的变化,由原来的方法区转移到了堆区,但永久代实际上还是存在的,因为内加载机制还在永久代。我们也知道,堆区是GC管理的主要区域,而常量池存储了该类的所有常量,如果使其处于永久代不进行及时的回收,导致内存溢出可能会是经常的。放入堆区个人理解就是为了方便及时合理回收运行时常量池。在jdk1.8之后,完全消除永久代这一个区域,jvm将方法区放入了一个与堆不相连的区域,该区域称为元空间(元空间介绍引用:https://www.cnblogs.com/duanxz/p/3520829.html)。


     以此代码为例,通过设置不同的虚拟机启动参数来探究内存溢出异常:




jdk1.6处于永久代的常量池所出现的OutOfMemoryError异常。我们设置永久代内存大小为10M(-XX:PermSize=10M -XX:MaxPermSize=10M)测试结果如下:




由以上可以看出,在jdk1.6之前运行时常量池出现内存溢出异常在PermGen space,也就是方法区。


jdk1.7运行时常量池放入堆区。我们设置堆内存大小为20M(-Xmx20M -Xms20M),测试结果如下:




说明jdk1.7中,运行时常量池在堆区。


四、本机直接内存溢出

     本机直接内存容量可以通过-XX:MaxDirectMemorySize指定,如果不指定,则默认与Java堆最大值(-Xmx制定)一样。NIO提供了一个不经过JVM直接访问本机物理内存的类——DirectMemory。DircetMemory继承与ByteBuffer,但和普通的ByteBuffer不同,普通的ByteBuffer在堆上进行内存分配,其最大的内存受到堆内存的限制。而DircetMemory是向本机物理内存申请内存分配,所以其大小只受物理内存的限制。由于直接内存是介于堆区和操作系统之间的一个Buffer,所以读写操作比普通的Buffer要快,同样也造成创建和销毁较普通Buffer慢的特点。以下代码清单中直接越过DircetByteBuffer(存储在Java堆上的操作直接内存的一个对象引用)类,直接通过反射获取unsafe实例进行内存分配。因为虽然使用了DircetByteBuffer分配内存也会抛出内存溢出异常,但它抛出异常时并没有真正向操作系统申请分配内存,而是通过计算得知内存无法分配,于是手动抛出异常,真正申请分配内存的方法是unsafe.allocateMemory()。




回答: 2021-11-09 10:36

211

一周热门 更多>