常见的OOM异常
在《Java虚拟机规范》的规定里,除了程序计数器外,虚拟机内存的其他几个运行时区域都有发生 OutOfMemoryError 异常的可能,本章主要介绍相关的OOM异常。
概述
常见的OOM异常为:
StackOverflowError
堆栈溢出,一种常见的递归调用过深引起的异常。Java heap space
最常见的异常。GC overhead limit exceeded
GC回收的耗时过长,且频繁。Direct buffer memory
本地直接内存溢出。unable to create new native thread
无法创建更多线程。Metaspace
方法区溢出。Requested array size exceeds VM limit
创建的数组超过的JVM最大长度限制。Out of swap space
Kill process or sacrifice child
操作系统层面的。
堆栈溢出
比较常见的是一些需要递归调用的时候,由于每次函数调用会在当前线程生成一个栈帧,当调用过深的时候,导致栈溢出。
如常见的斐波那契数列的计算,如果不判断初始条件,则很容易出现这个问题:
1 | private static int feb(int i) { |
出现的原因
- 无限递归循环调用;
- 执行大量的方法,导致线程栈空间用完;
- 方法内有海量的局部变量,局部变量表是栈帧的一部分。
解决方案
- 控制递归的终止条件,避免无限递归调用;
- 检查类之间的循环依赖,(当两个对象相互引用的时候,调用tostring方法容易出现);
- 通过JVM参数
-Xss
适当增加线程栈内存大小。
Java heap space
最常见的OOM异常,一般是堆的空间已经容不下新生成的对象了。
出现原因
- 创建一个超大对象;
- 对象来不及GC,又有新的对象生成;
- 内存泄漏(Memory Leak),大量的对象引用没有释放,JVM无法回收空间,常见一些资源用完没有关闭。
注:内存溢出(Out of memory)是申请内存,没有足够的内存使用,会出现;内存泄漏(memory leak)指申请内存后没有释放,终将导致内存溢出。
解决方案
- 通过调大
-Xmx
参数,来增加堆空间的大小; - 检查是否有超大对象,比如超大数组,查询全部的数据;
- 及时关闭没有使用的资源,如一些文件流。
GC overhead limit exceeded
当 Java 进程花费 98% 以上的时间执行 GC,但只恢复了不到 2% 的内存,且该动作连续重复了 5 次,就会抛出java.lang.OutOfMemoryError:GC overhead limit exceeded
错误(俗称:垃圾回收上头)。简单地说,就是应用程序已经基本耗尽了所有可用内存, GC 也无法回收。
即使不抛出异常,程序会一直GC,也会出现CPU占用100%,进入假死。
栗子:
1 | // 一个死循环 |
设置JVM参数为-Xms512m -Xmx512m
,运行程序,过一会会出现Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
。
当然可能还会出现Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
的异常。
出现原因
- 内存泄漏;
- 循环创建大量对象。
解决方案
- 添加 JVM 参数
-XX:-UseGCOverheadLimit
不推荐这么干,没有真正解决问题,只是将异常推迟; - 检查项目中是否有大量的死循环或有使用大内存的代码,优化代码;
- 增大堆内存。
Direct buffer memory
使用 NIO 的时候经常需要使用 ByteBuffer 来读取或写入数据,这是一种基于 Channel(通道) 和 Buffer(缓冲区)的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆里面的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样在一些场景就避免了 Java 堆和 Native 中来回复制数据,所以性能会有所提高。ByteBuffer.allocateDirect(capability)
是分配 OS 本地内存,不属于 GC 管辖范围,由于不需要内存拷贝所以速度相对较快。
出现原因
如果不断分配本地内存,堆内存很少使用,那么 JVM 就不需要执行 GC,DirectByteBuffer 对象就不会被回收,这时虽然堆内存充足,但本地内存可能已经不够用了,就会出现 OOM,本地直接内存溢出。
解决方案
- 通过启动参数
-XX:MaxDirectMemorySize
调整 Direct ByteBuffer 的上限值; - 检查堆外内存使用代码,确认是否存在内存泄漏;
- 物理内存确实太小;
- 检查是否直接或间接使用了 NIO,如 netty,jetty 等。
Unable to create new native thread
每个 Java 线程都需要占用一定的内存空间,当 JVM 向底层操作系统请求创建一个新的 native 线程时,如果没有足够的资源分配就会报此类错误。
出现原因
- 线程数超过操作系统最大线程数限制;
- 本地内存不足。
解决方案
- 想办法降低程序中创建线程的数量,分析应用是否真的需要创建这么多线程;
- 如果确实需要创建很多线程,调高 OS 层面的线程最大数:执行
ulimia-a
查看最大线程数限制,使用ulimit-u xxx
调整最大线程数限制。
Metaspace
jdk1.8后,方法区是元空间,在本地内存中存储,也可以通过参数设置大小-XX:MetaspaceSize -XX:MaxMetaspaceSize
,当加载过多的类的时候,本地内存耗尽后会出现。
出现原因
- 代理生成大量的动态类。
解决方法
-XX:MetaspaceSize -XX:MaxMetaspaceSize
设置元空间的初始大小和最大值。
Requested array size exceeds VM limit
JVM 限制了数组的最大长度,该错误表示程序请求创建的数组超过最大长度限制。