OutOfMemoryError

以下介绍常见的OOM原因及解决方法

  1. Java heap space
    当堆内存(Heap Space)没有足够空间存放新创建的对象时,就会抛出 java.lang.OutOfMemoryError: Java heap space 错误(根据实际生产经验,可以对程序日志中的 OutOfMemoryError 配置关键字告警,一经发现,立即处理)。

原因分析
Java heap space 错误产生的常见原因可以分为以下几类:

1.请求创建一个超大对象,通常是一个大数组。

2.超出预期的访问量/数据量,通常是上游系统请求流量飙升,常见于各类促销/秒杀活动,可以结合业务流量指标排查是否有尖状峰值。

3.过度使用终结器(Finalizer),该对象没有立即被 GC。

4.内存泄漏(Memory Leak),大量对象引用没有释放,JVM 无法对其自动回收,常见于使用了 File 等资源没有回收。

解决方案

1.针对大部分情况,通常只需要通过 -Xmx 参数调高 JVM 堆内存空间即可。如果仍然没有解决,可以参考以下情况做进一步处理:

2.如果是超大对象,可以检查其合理性,比如是否一次性查询了数据库全部结果,而没有做结果数限制。

3.如果是业务峰值压力,可以考虑添加机器资源,或者做限流降级。

4.如果是内存泄漏,需要找到持有的对象,修改代码设计,比如关闭没有释放的连接。



  1. GC overhead limit exceeded
    当 Java 进程花费 98% 以上的时间执行 GC,但只恢复了不到 2% 的内存,且该动作连续重复了 5 次,就会抛出 java.lang.OutOfMemoryError:GC overhead limit exceeded 错误。简单地说,就是应用程序已经基本耗尽了所有可用内存, GC 也无法回收。

此类问题的原因与解决方案跟 Java heap space 非常类似,可以参考上文。



  1. Permgen space
    该错误表示永久代(Permanent Generation)已用满,通常是因为加载的 class 数目太多或体积太大。

原因分析
永久代存储对象主要包括以下几类:

1.加载/缓存到内存中的 class 定义,包括类的名称,字段,方法和字节码;

2.常量池;

3.对象数组/类型数组所关联的 class;

4.JIT 编译器优化后的 class 信息。

PermGen 的使用量与加载到内存的 class 的数量/大小正相关。

解决方案
根据 Permgen space 报错的时机,可以采用不同的解决方案,如下所示:

1.程序启动报错,修改 -XX:MaxPermSize 启动参数,调大永久代空间。

2.应用重新部署时报错,很可能是应用没有重启,导致加载了多份 class 信息,只需重启 JVM 即可解决。

3.运行时报错,应用程序可能会动态创建大量 class,而这些 class 的生命周期很短暂,但是 JVM 默认不会卸载 class,可以设置
-XX:+CMSClassUnloadingEnabled 和
-XX:+UseConcMarkSweepGC 这两个参数允许 JVM 卸载 class。

如果上述方法无法解决,可以通过 jmap 命令 dump 内存对象 jmap -dump:format=b,file=dump.hprof ,然后利用 Eclipse MAT 功能逐一分析开销最大的 classloader 和重复 class。

  1. Metaspace
    JDK 1.8 使用 Metaspace 替换了永久代(Permanent Generation),该错误表示 Metaspace 已被用满,通常是因为加载的 class 数目太多或体积太大。

此类问题的原因与解决方法跟 Permgen space 非常类似,可以参考上文。需要特别注意的是调整 Metaspace 空间大小的启动参数为 -XX:MaxMetaspaceSize。



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

原因分析
JVM 向 OS 请求创建 native 线程失败,就会抛出 Unable to create new native thread,常见的原因包括以下几类:

1.线程数超过操作系统最大线程数 ulimit 限制。
2.线程数超过 kernel.pid_max(只能重启)。
3.native 内存不足。

该问题发生的常见过程主要包括以下几步:
JVM 内部的应用程序请求创建一个新的 Java 线程;
JVM native 方法代理了该次请求,并向操作系统请求创建一个 native 线程;
操作系统尝试创建一个新的 native 线程,并为其分配内存;
如果操作系统的虚拟内存已耗尽,或是受到 32 位进程的地址空间限制,操作系统就会拒绝本次 native 内存分配;
JVM 将抛出 java.lang.OutOfMemoryError: Unable to create new native thread 错误。

解决方案
1.升级配置,为机器提供更多的内存;
2.降低 Java Heap Space 大小;
3.修复应用程序的线程泄漏问题;
4.限制线程池大小;
5.使用 -Xss 参数减少线程栈的大小;
6.调高 OS 层面的线程最大数:执行 ulimia -a 查看最大线程数限制,使用 ulimit -u xxx 调整最大线程数限制。

ulimit -a
… 省略部分内容 …
max user processes (-u) 16384



StackOverflowError

StackOverflowError 是一个java中常出现的错误:在jvm运行时的数据区域中有一个java虚拟机栈,当执行java方法时会进行压栈弹栈的操作。在栈中会保存局部变量,操作数栈,方法出口等等。jvm规定了栈的最大深度,当执行时栈的深度大于了规定的深度,就会抛出StackOverflowError错误。

如果某个线程的线程栈空间被耗尽,没有足够资源分配给新创建的栈帧,就会抛出 java.lang.StackOverflowError 错误。

首先给出一个简单的程序调用代码示例,如下所示:

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() 方法被调用后,执行线程按照代码执行顺序,将它正在执行的方法、基本数据类型、对象指针和返回值包装在栈帧中,逐一压入其私有的调用栈,整体执行过程如下图所示:

java heapdump分析 java heap out of memory_java heapdump分析

首先,程序启动后,main() 方法入栈。

然后,a() 方法入栈,变量 x 被声明为 int 类型,初始化赋值为 0。注意,无论是 x 还是 0 都被包含在栈帧中。

接着,b() 方法入栈,创建了一个 Car 对象,并被赋给变量 y。请注意,实际的 Car 对象是在 Java 堆内存中创建的,而不是线程栈中,只有 Car 对象的引用以及变量 y 被包含在栈帧里。

最后,c() 方法入栈,变量 z 被声明为 float 类型,初始化赋值为 0f。同理,z 还是 0f 都被包含在栈帧里。

当方法执行完成后,所有的线程栈帧将按照后进先出的顺序逐一出栈,直至栈空为止。

StackOverFlowError 是如何产生的?

如上所述,JVM 线程栈存储了方法的执行过程、基本数据类型、局部变量、对象指针和返回值等信息,这些都需要消耗内存。一旦线程栈的大小增长超过了允许的内存限制,就会抛出 java.lang.StackOverflowError 错误。

下面这段代码通过无限递归调用最终引发了

java.lang.StackOverflowError 错误。

public class StackOverflowErrorExample {
	public static void main(String args[]) {
            a();
      }
	public static void a() {
            a();
      }
}

在这种情况下,a() 方法将无限入栈,直至栈溢出,耗尽线程栈空间,如下图所示。

java heapdump分析 java heap out of memory_jvm_02

如何解决 StackOverFlowError?

引发 StackOverFlowError 的常见原因有以下几种:

1.无限递归循环调用(最常见)。

2.执行了大量方法,导致线程栈空间耗尽。

3.方法内声明了海量的局部变量。

4.native 代码有栈上分配的逻辑,并且要求的内存还不小,比如 java.net.SocketInputStream.read0 会在栈上要求分配一个 64KB 的缓存(64位 Linux)。

常见的解决方法包括以下几种:

1.修复引发无限递归调用的异常代码, 通过程序抛出的异常堆栈,找出不断重复的代码行,按图索骥,修复无限递归 Bug。

2.排查是否存在类之间的循环依赖。

3.排查是否存在在一个类中对当前类进行实例化,并作为该类的实例变量。

4.通过 JVM 启动参数 -Xss 增加线程栈内存空间, 某些正常使用场景需要执行大量方法或包含大量局部变量,这时可以适当地提高线程栈空间限制,例如通过配置 -Xss2m 将线程栈空间调整为 2 mb。

线程栈的默认大小依赖于操作系统、JVM 版本和供应商,常见的默认配置如下表所示:

JVM 版本 线程栈 默认大小
Sparc 32-bit JVM 512 kb
Sparc 64-bit JVM 1024 kb
x86 Solaris/Linux 32-bit JVM 320 kb
x86 Solaris/Linux 64-bit JVM 1024 kb
Windows 32-bit JVM 320 kb
Windows 64-bit JVM 1024 kb

推荐检测工具

ARMS —— 阿里云 APM 产品,支持 StackOverFlowError 异常关键字告警