OOM:OutOfMemory (内存溢出)
public class OutOfMemoryError extends VirtualMachineError {
}

OOM分类
一.StackOverFlowError(线程栈空间耗尽)
每一个 JVM 线程都拥有一个私有的 JVM 线程栈,用于存放当前线程的 JVM 栈帧(包括被调用函数的参数、局部变量和返回地址等)。
如果某个线程的线程栈空间被耗尽,没有足够资源分配给新创建的栈帧,就会抛出 java.lang.StackOverflowError 错误
1.线程栈是如何运行的?代码示例:
public class SimpleExample {
public static void main(String args[]) {
a();
}
public static void a() {
int x = 0;
b();
}
public static void b() {
Car y = new Car();
c();
}
public static void c() {
float z = 0f;
}
}
当 main() 方法被调用后,执行线程按照代码执行顺序,将它正在执行的方法、基本数据类型、对象指针和返回值包装在栈帧中,逐一压入其私有的调用栈,整体执行过程如下图所示:

1.首先,程序启动后,main() 方法入栈。
2.然后,a() 方法入栈,变量 x 被声明为 int 类型,初始化赋值为 0。注意,无论是 x 还是 0 都被包含在栈帧中。
3.接着,b() 方法入栈,创建了一个 Car 对象,并被赋给变量 y。请注意,实际的 Car 对象是在 Java 堆内存中创建的,而不是线程栈中,只有 Car 对象的引用以及变量 y 被包含在栈帧里。
4.最后,c() 方法入栈,变量 z 被声明为 float 类型,初始化赋值为 0f。同理,z 还是 0f 都被包含在栈帧里。
5.当方法执行完成后,所有的线程栈帧将按照后进先出的顺序逐一出栈,直至栈空为止
2.引发 StackOverFlowError 的常见原因有以下几种:
1.无限递归循环调用(最常见)。
2.执行了大量方法,导致线程栈空间耗尽。
3.方法内声明了海量的局部变量(局部变量会增加栈内存的开销)。
4.native 代码有栈上分配的逻辑,并且要求的内存还不小,比如 java.net.SocketInputStream.read0 会在栈上要求分配一个 64KB 的缓存(64位 Linux)。
Tips:成员变量和局部变量
成员变量就是方法外部,类的内部定义的变量,存储在堆中的对象里面,由垃圾回收器负责回收;
局部变量就是方法或语句块内部定义的变量。局部变量必须初始化。 形式参数是局部变量,局部变量的数据存在于栈内存中。栈内存中的局部变量随着方法的消失而消失
3.除了程序抛出 StackOverflowError 错误以外,还有两种定位栈溢出的方法:
1.进程突然消失,但是留下了 crash 日志,可以检查 crash 日志里当前线程的 stack 范围,以及 RSP 寄存器的值。如果 RSP 寄存器的值超出这个 stack 范围,那就说明是栈溢出了。
2.如果没有 crash 日志,那只能通过 coredump 进行分析。在进程运行前,先执行 ulimit -c unlimited,当进程挂掉之后,会产生一个 core.[pid] 的文件,然后再通过 jstack $JAVA_HOME/bin/java core.[pid] 来看输出的栈。
如果正常输出了,那就可以看是否存在很长的调用栈的线程,当然还有可能没有正常输出的,因为 jstack 的这条从 core 文件抓栈的命令其实是基于 Serviceability Agent 实现的,而 SA 在某些版本里有 Bug。
4.常见的解决栈溢出的方法包括以下几种:
修复引发无限递归调用的异常代码, 通过程序抛出的异常堆栈,找出不断重复的代码行,按图索骥,修复无限递归 Bug。
排查是否存在类之间的循环依赖。
排查是否存在在一个类中对当前类进行实例化,并作为该类的实例变量。
通过 JVM 启动参数 -Xss 增加线程栈内存空间, 某些正常使用场景需要执行大量方法或包含大量局部变量,这时可以适当地提高线程栈空间限制,例如通过配置 -Xss2m 将线程栈空间调整为 2 mb
提示: 实际生产系统中,可以对程序日志中的 StackOverFlowError 配置关键字告警,一经发现,立即处理。
二.Java heap space
发生java.lang.OutOfMemoryError: Java heap space异常时,代表着应用尝试从堆上申请一个区域时,堆没有可配的空间。(可能有可使用的物理内存,但是没有已经达到了JAVA应用可分配的内存大小)
JVM是很智能的,在即将发生OOM时,会进行一次FullGC以回收可回收的对象来释放空间。
如果FullGC之后还是没有可满足大小的空间分配,才抛出java.lang.OutOfMemoryError: Java heap space。
1.java.lang.OutOfMemoryError: Java heap space正常是怎么发生的呢?
突发高峰期:程序在正常的用户量和一定数据量时运行正常。但是,在某个高峰时导致超出预期阈值,内存存活对象使用空间的量超出最大堆,并且无法回收。
内存泄露: 由于编程错误导致应用程序不再需要的对象(数据)一直被持有引用,导致无法被回收。随着时间的推移,泄露的内存对象占用了所有的可用堆空间
2.Java heap space解决方法?
1、设置初始堆内存-Xms,设置最大堆内存-Xmx(JVM在启动的时候会自动设置Heap size的值,其初始空间(即-Xms)是物理内存的1/64,最大空间(-Xmx)是物理内存的1/4)
2、尽早释放无用对象的引用
3、给JVM分配足够大的内存来满足运行程序的需求(分配一个合理的内存空间,针对GC(垃圾回收)进行优化,也就是常说的JVM调优)
三.GC overhead limit exceeded
简单理解:GC回收时间过长,时间过多耗费在GC中,但是回收效果不佳。
1.原理:
回收过长指的是超过98%的时间用来做GC,并且回收了不到2%的堆内存,
连续多次GC,都只回收了不到2%的极端情况下才会抛出异常,
如不抛出异常,GC清理后的内存也会很快再次填满,迫使GC再次执行,
就此形成恶性循环,导致内存不足,从而产生异常
例如:下载数据时文件大小超过某一峰值是会报这个错误。
原因是在页面点击下载时,在数据库查询了很庞大的数据量,导致内存使用增加,才会出现这个问题
2.解决办法:
1,查看项目中是否有大量的死循环或有使用大内存的代码,优化代码。
2,JVM给出这样一个参数:-XX:-UseGCOverheadLimit 禁用这个检查,
其实这个参数解决不了内存问题,只是把错误的信息延后,替换成 java.lang.OutOfMemoryError: Java heap space。
3,增大堆内存 set JAVA_OPTS=-server -Xms512m -Xmx1024m -XX:MaxNewSize=1024m -XX:MaxPermSize=1024m
四.Direct buffer memory
直接内存溢出(直接操作内存导致,我们程序中直接或者间接的使用NIO可能会导致此类异常的产生)
1.原因分析:
直接内存崩溃,此处元空间并不在虚拟机中,而是使用本地内存,与GC无关。
常见于NIO程序中,使用ByteBuffer来读取和写入数据,这是基于通道channel和缓冲区buffer的IO方式,
可以使用Native函数直接分配堆外内存,通过存储在JAVA堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。
该方式在某些常见中能提高性能,因为避免了java堆和native堆中来回拷贝数据
例如
ByteBuffer.allocate(capability) 堆内内存 属于GC管辖,由于需要拷贝所以速度相对较慢
ByteBuffer.allocateDirect(capability) 本地内存 不属于GC管辖,由于不需要拷贝所以速度较快
如果不断分配本地内存,堆内存很少使用,那么JVM就不需要执行GC,DirectByteBuffer对象们就不会被回收,此时堆内存充足,但是本地内存即将耗尽,那么再次尝试分配本地内存就会出现OOM
2.解决办法:
1)检查是否直接或间接使用了 nio ,例如手动调用生成 buffer 的方法或者使用了 nio 容器如 netty, jetty, tomcat 等等;
2)-XX:MaxDirectMemorySize 加大,该参数默认是 64M ,可以根据需求调大试试;
3)检查 JVM 参数里面有无: -XX:+DisableExplicitGC ,如果有就去掉.
Tips:NIO
NIO (New I/O):同时支持阻塞与非阻塞模式(文件channel只支持阻塞模式,socket的channel支持阻塞和非阻塞模式),
以其同步非阻塞I/O模式来说明,那么什么叫做同步非阻塞?
如果还拿烧开水来说,NIO的做法是叫一个线程不断的轮询每个水壶的状态,看看是否有水壶的状态发生了改变,从而进行下一步的操作。
(特点就是线程不必等待IO读写完成,在IO进行过程中线程可以不停地轮询IO的状态,一旦发现IO状态变化,就可以做出相应处理)
五.unable to create new native thread
高并发情况下会出现该异常,该异常与对应的平台有关
1.原因分析?
一个应用进程创建太多的线程,超过系统承载极限。如Linux默认允许单个进程可以创建的线程数是1024个。
2.解决方法?
降低应用程序创建线程的数量,分析应用是否真的需要创建那么多线程,将线程数降到最低
修改服务器配置,如修改Linux服务器配置,扩大Linux默认限制(例如修改对应用户的线程限制)
六.Metaspace (元空间内存溢出)
1.元空间存放的数据?
1.虚拟机加载的类信息
2.常量池
3.静态变量
4.即时编译后的代码
元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制
2.为什么 JDK 8 中永久代转换为元空间?
1、字符串存在永久代中,容易出现性能问题和内存溢出。
2、类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。
3、永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
4、Oracle 可能会将HotSpot 与 JRockit 合二为一
3.什么情况下会发生MetaSpace内存溢出?
一般而言,这块发生的内存溢出的概率非常的小,一般由于2点导致:
1.在JVM正式环境,直接使用JDK自带的参数,一般默认的只有几十MB,针对大型的应用项目,很容易不够,报异常。
2.很多人在写cglib之类的技术动态生成一些类,如果代码没有控制好回收,容易引发MetaSpace溢出
Tips:
在cmd中执行 以下命令可以查看默认的JVM参数基本配置,可以查看metaspaceSize的大小
java -XX:+PrintFlagsInitial
用于个人学习记录
转载自
本文详细解析了Java中的各种内存溢出问题,包括StackOverFlowError、Java heap space、GC overhead limit exceeded等异常的原因、触发条件及解决方案。
1083

被折叠的 条评论
为什么被折叠?



