优秀的编程知识分享平台

网站首页 > 技术文章 正文

OutOfMemory错误大家都知道,但是你真的了解吗?详细分析在这里

nanyue 2024-10-12 05:45:17 技术文章 5 ℃

OutOfMemoryError是JDK中的一个系统错误,注意它是一个Error不是Exception。所以我们常见的catch Exception是无法catch到它的。这个Error几乎每一个Java开发这都有所了解,但是并不是每个人的系统都有机会遇到它,今天我们就来好好分析分析这个OutOfMemoryError,下面的内容肯定有你不知道的!

OOM错误的类关系

上图展示了OutOfMemoryError类的继承关系,可以看到它是从Error继承而来的,而不是从Exception继承的。所以catch Exception是无法catch到OOM异常的。由于Error是继承于Throwable的,所以如果catch Throwable的话是可以catch到OOM错误的(最好不要这么干!!!)。

构造一个OOM错误

要想了解OOM错误,我们就需要先写段代码来抛出这个错误。当要分配内存的时候如果内存不够就会抛出这个错误,下面的代码会往List中不断插入字符串,显然它最终会耗尽所有的内存,从而抛出OOM错误:

有了上面的代码还不是很够,我们还需要限制JVM的可用内存大小,这样运行的时候就快速的抛出异常。所以我们需要在VM启动参数中加入如下参数:

接下来运行这段java代码我们就可以马上得到OOM错误了:

OOM错误和我们的普通的异常有什么不同

从上面的错误信息来看,OOM错误貌似和我们平时遇到的各种Exception并没有太大的不同:

  • 都是出现了无法处理的异常情况,然后向上抛出,如果没有catch就一直向上抛知道程序推出
  • 他们都记录了错误发生是的堆栈信息(真的吗)

既然OOM作为一个Error独立存在,那么显然它和我们平时常见的Exception肯定是有不同的。首先我把这些不同之处列出来,后面慢慢分析。

  1. OutOfMemory类并不是等到需要抛异常的时候才被加载到内存。
  2. 并不是每一个OOM错误都会带上异常堆栈的,也就是说很多OOM错误是不会记录案发现场的。
  3. 一般的异常都是需要throw的时候new出来的,而OOM是在JVM初始化的时候统一实例化的,后续都是用的最初实例化的哪些OOM对象。

解析来进一步分析

OutOfMemory实例化过程

上面代码就是JVM里启动的时候先创建一批OOM实例的代码,后续的时候如果要抛OOM错误的时候就是直接使用这些实例了。

既然在JVM启动的时候就创建了OutOfMemory对象,那么这个类肯定在JVM启动的时候就已经加载完毕了,后续也不会在出现new的情况了。

为什么有些OOM不会带上案发现场

首先我们看看如下的代码:

这段代码和上面的代码唯一的一点就是catch了Throwable,也就是OOM也能够被catch到。然后我们在看看它的输出:

从上面的输出结果我们可以看出,OOM只有在前4次才会输出调用堆栈信息,后续就没有任何调用堆栈了。

调用堆栈对于OOM错误重不重要?其实并不重要,因为往往引起OOM错误的并不是最终爆出OOM的地方,那个时候只是内存被耗尽了而已,所以对于定位问题并不关键,所以这个堆栈信息就不重要了。

JVM是如何实现OOM错误只有前4次才有调用堆栈的

要想知道原因,还需要再回到前文提到的初始化函数:universe_post_init。它还有一些剩余代码上面没有贴出来:

这段代码会再创建几个OOM对象,个数有是代码里写死的4个。后续当要抛出异常的时候会先看看这个列表里是否还有可用的OOM对象,如果还有就会取出来用,然后组装好相关的错误信息和调用堆栈抛出去。这个列表的对象是一次性的,使用过后就会丢弃的。由于总共只有4个,所以上面的示例代码只有前4次输出了调用堆栈。

OOM错误如何分析解决

上面讲了很多都是OOM实现的基本原理,虽然我们日常工作中比较少见到OOM。但是遇到OOM基本都是需要快速解决的大问题了。

所以我们可以对仅有的一点错误信息进行分析,上面的代码中可以知道不同区域的内存溢出抛出的OOM对象是不一样的,错误信息也是有所不同的,所以我们可以根据这些错误信息分析出是哪个区域内存溢出了。比如上面的示例代码中的:Java heap space表示的就是堆内存溢出了。如果错误信息是:PermGen space表示的就是Perm区溢出了。

确定好出错的区域后就可以根据不同的区域进行不同的操作了,比如比较常见的堆内存溢出就可以dump出内存然后进行分析解决,那就将是另外一大片内容了,这里就不做进一步介绍了。

最近发表
标签列表