在《Java虚拟机规范》的规定里,除了程序计数器外,虚拟机内存的其他几个运行时区域都有发生 OutOfMemoryError 异常的可能,本章主要介绍相关的OOM异常。

概述

常见的OOM异常为:

  • StackOverflowError堆栈溢出,一种常见的递归调用过深引起的异常。
  • Java heap space最常见的异常。
  • GC overhead limit exceededGC回收的耗时过长,且频繁。
  • 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
2
3
4
5
6
7
8
9
10
11
private static int feb(int i) {
return i*feb(i-1);
}
public static void stackOverFlow(){
feb(1);
}
/**
Exception in thread "main" java.lang.StackOverflowError
at com.abumaster.javabase.jvm.TestOom.feb(TestOom.java:27)
at com.abumaster.javabase.jvm.TestOom.feb(TestOom.java:27)
*/

出现的原因

  1. 无限递归循环调用;
  2. 执行大量的方法,导致线程栈空间用完;
  3. 方法内有海量的局部变量,局部变量表是栈帧的一部分。

解决方案

  1. 控制递归的终止条件,避免无限递归调用;
  2. 检查类之间的循环依赖,(当两个对象相互引用的时候,调用tostring方法容易出现);
  3. 通过JVM参数-Xss适当增加线程栈内存大小。

Java heap space

最常见的OOM异常,一般是堆的空间已经容不下新生成的对象了。

出现原因

  • 创建一个超大对象;
  • 对象来不及GC,又有新的对象生成;
  • 内存泄漏(Memory Leak),大量的对象引用没有释放,JVM无法回收空间,常见一些资源用完没有关闭。

注:内存溢出(Out of memory)是申请内存,没有足够的内存使用,会出现;内存泄漏(memory leak)指申请内存后没有释放,终将导致内存溢出。

解决方案

  1. 通过调大-Xmx参数,来增加堆空间的大小;
  2. 检查是否有超大对象,比如超大数组,查询全部的数据;
  3. 及时关闭没有使用的资源,如一些文件流。

GC overhead limit exceeded

当 Java 进程花费 98% 以上的时间执行 GC,但只恢复了不到 2% 的内存,且该动作连续重复了 5 次,就会抛出java.lang.OutOfMemoryError:GC overhead limit exceeded 错误(俗称:垃圾回收上头)。简单地说,就是应用程序已经基本耗尽了所有可用内存, GC 也无法回收。
即使不抛出异常,程序会一直GC,也会出现CPU占用100%,进入假死。

栗子:

1
2
3
4
5
6
7
8
// 一个死循环
public static void addRandomDataToMap() {
Map<Integer, String> dataMap = new HashMap<>();
Random r = new Random();
while (true) {
dataMap.put(r.nextInt(), String.valueOf(r.nextInt()));
}
}

设置JVM参数为-Xms512m -Xmx512m,运行程序,过一会会出现Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
当然可能还会出现Exception in thread "main" java.lang.OutOfMemoryError: Java heap space的异常。

出现原因

  • 内存泄漏;
  • 循环创建大量对象。

解决方案

  1. 添加 JVM 参数-XX:-UseGCOverheadLimit不推荐这么干,没有真正解决问题,只是将异常推迟;
  2. 检查项目中是否有大量的死循环或有使用大内存的代码,优化代码;
  3. 增大堆内存。

Direct buffer memory

使用 NIO 的时候经常需要使用 ByteBuffer 来读取或写入数据,这是一种基于 Channel(通道) 和 Buffer(缓冲区)的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆里面的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样在一些场景就避免了 Java 堆和 Native 中来回复制数据,所以性能会有所提高。
ByteBuffer.allocateDirect(capability)是分配 OS 本地内存,不属于 GC 管辖范围,由于不需要内存拷贝所以速度相对较快。

出现原因

如果不断分配本地内存,堆内存很少使用,那么 JVM 就不需要执行 GC,DirectByteBuffer 对象就不会被回收,这时虽然堆内存充足,但本地内存可能已经不够用了,就会出现 OOM,本地直接内存溢出。

解决方案

  1. 通过启动参数-XX:MaxDirectMemorySize调整 Direct ByteBuffer 的上限值;
  2. 检查堆外内存使用代码,确认是否存在内存泄漏;
  3. 物理内存确实太小;
  4. 检查是否直接或间接使用了 NIO,如 netty,jetty 等。

Unable to create new native thread

每个 Java 线程都需要占用一定的内存空间,当 JVM 向底层操作系统请求创建一个新的 native 线程时,如果没有足够的资源分配就会报此类错误。

出现原因

  • 线程数超过操作系统最大线程数限制;
  • 本地内存不足。

解决方案

  1. 想办法降低程序中创建线程的数量,分析应用是否真的需要创建这么多线程;
  2. 如果确实需要创建很多线程,调高 OS 层面的线程最大数:执行ulimia-a查看最大线程数限制,使用ulimit-u xxx调整最大线程数限制。

Metaspace

jdk1.8后,方法区是元空间,在本地内存中存储,也可以通过参数设置大小-XX:MetaspaceSize -XX:MaxMetaspaceSize,当加载过多的类的时候,本地内存耗尽后会出现。

出现原因

  • 代理生成大量的动态类。

解决方法

  1. -XX:MetaspaceSize -XX:MaxMetaspaceSize设置元空间的初始大小和最大值。

Requested array size exceeds VM limit

JVM 限制了数组的最大长度,该错误表示程序请求创建的数组超过最大长度限制。

out of swap space

参考文档

Kill process or sacrifice child

参考文档

附录