Java 内存溢出(OOM)异常完全指南 您所在的位置:网站首页 内存泄露是永久的吗 Java 内存溢出(OOM)异常完全指南

Java 内存溢出(OOM)异常完全指南

2023-09-12 19:23| 来源: 网络整理| 查看: 265

java.lang.OutOfMemoryError: Java heap space

Java 应用程序在启动时会指定所需要的内存大小,它被分割成两个不同的区域:Heap space(堆空间)和Permgen(永久代):

这两个区域的大小可以在 JVM(Java 虚拟机)启动时通过参数-Xmx和-XX:MaxPermSize设置,如果你没有显式设置,则将使用特定平台的默认值。

当应用程序试图向堆空间添加更多的数据,但堆却没有足够的空间来容纳这些数据时,将会触发java.lang.OutOfMemoryError: Java heap space异常。需要注意的是:即使有足够的物理内存可用,只要达到堆空间设置的大小限制,此异常仍然会被触发。

原因分析

触发java.lang.OutOfMemoryError: Java heap space最常见的原因就是应用程序需要的堆空间是 XXL 号的,但是 JVM 提供的却是 S 号。解决方法也很简单,提供更大的堆空间即可。除了前面的因素还有更复杂的成因:

流量/数据量峰值:应用程序在设计之初均有用户量和数据量的限制,某一时刻,当用户数量或数据量突然达到一个峰值,并且这个峰值已经超过了设计之初预期的阈值,那么以前正常的功能将会停止,并触发java.lang.OutOfMemoryError: Java heap space异常。内存泄漏:特定的编程错误会导致你的应用程序不停的消耗更多的内存,每次使用有内存泄漏风险的功能就会留下一些不能被回收的对象到堆空间中,随着时间的推移,泄漏的对象会消耗所有的堆空间,最终触发java.lang.OutOfMemoryError: Java heap space错误。示例简单示例

首先看一个非常简单的示例,下面的代码试图创建2 x 1024 x 1024个元素的整型数组,当你尝试编译并指定 12M 堆空间运行时(-Xmx12m)将会失败并抛出java.lang.OutOfMemoryError: Java heap space错误,而当你指定 13M 堆空间时,将正常的运行。

class OOM { static final int SIZE=2*1024*1024; public static void main(String[] a) { int[] i = new int[SIZE]; } }

运行如下:

D:\>javac OOM.java D:\>java -Xmx12m OOM Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at OOM.main(OOM.java:4) D:\>java -Xmx13m OOM内存泄漏示例

在 Java 中,当开发者创建一个新对象(比如new Integer(5))时,不需要自己开辟内存空间,而是把它交给 JVM。在应用程序整个生命周期类,JVM 负责检查哪些对象可用,哪些对象未被使用。未使用对象将被丢弃,其占用的内存也将被回收,这一过程被称为垃圾回收。JVM 负责垃圾回收的模块集合被称为垃圾回收器。

Java 的内存自动管理机制依赖于 GC 定期查找未使用对象并删除它们。Java 中的内存泄漏是由于 GC 无法识别一些已经不再使用的对象,而这些未使用的对象一直留在堆空间中,这种堆积最终会导致java.lang.OutOfMemoryError: Java heap space错误。

我们可以非常容易的写出导致内存泄漏的 Java 代码:

public class KeylessEntry { static class Key { Integer id; Key(Integer id) { this.id = id; } @Override public int hashCode() { return id.hashCode(); } } public static void main(String[] args) { Map m = new HashMap(); while(true) { for(int i=0;i= 0; i--) { try { int[] arr = new int[Integer.MAX_VALUE-i]; System.out.format("Successfully initialized an array with %,d elements.\n", Integer.MAX_VALUE-i); } catch (Throwable t) { t.printStackTrace(); } }

该示例重复四次,并在每个回合中初始化一个长原语数组。 该程序尝试初始化的数组的大小在每次迭代时增加 1,最终达到Integer.MAX_VALUE。 现在,当使用 Hotspot 7 在 64 位 Mac OS X 上启动代码片段时,应该得到类似于以下内容的输出:

java.lang.OutOfMemoryError: Java heap space at eu.plumbr.demo.ArraySize.main(ArraySize.java:8) java.lang.OutOfMemoryError: Java heap space at eu.plumbr.demo.ArraySize.main(ArraySize.java:8) java.lang.OutOfMemoryError: Requested array size exceeds VM limit at eu.plumbr.demo.ArraySize.main(ArraySize.java:8) java.lang.OutOfMemoryError: Requested array size exceeds VM limit at eu.plumbr.demo.ArraySize.main(ArraySize.java:8)

注意,在出现Requested array size exceeded VM limit之前,出现了更熟悉的java.lang.OutOfMemoryError: Java heap space。 这是因为初始化2 ^ 31 - 1个元素的数组需要腾出 8G 的内存空间,大于 JVM 使用的默认值。

解决方案

java.lang.OutOfMemoryError: Requested array size exceeds VM limit可能会在以下任一情况下出现:

数组增长太大,最终大小在平台限制和Integer.MAX_INT之间你有意分配大于2 ^ 31 - 1个元素的数组

在第一种情况下,检查你的代码库,看看你是否真的需要这么大的数组。也许你可以减少数组的大小,或者将数组分成更小的数据块,然后分批处理数据。

在第二种情况下,记住 Java 数组是由int索引的。因此,当在平台中使用标准数据结构时,数组不能超过2 ^ 31 - 1个元素。事实上,在编译时就会出错:error:integer number too large。

Out of memory: Kill process or sacrifice child

为了理解这个错误,我们需要补充一点操作系统的基础知识。操作系统是建立在进程的概念之上,这些进程在内核中作业,其中有一个非常特殊的进程,名叫“内存杀手(Out of memory killer)”。当内核检测到系统内存不足时,OOM killer 被激活,然后选择一个进程杀掉。哪一个进程这么倒霉呢?选择的算法和想法都很朴实:谁占用内存最多,谁就被干掉。如果你对 OOM Killer 感兴趣的话,建议你阅读参考资料 ② 中的文章。

当可用虚拟虚拟内存(包括交换空间)消耗到让整个操作系统面临风险时,就会产生Out of memory: Kill process or sacrifice child错误。在这种情况下,OOM Killer 会选择“流氓进程”并杀死它。

原因分析

默认情况下,Linux 内核允许进程请求比系统中可用内存更多的内存,但大多数进程实际上并没有使用完他们所分配的内存。这就跟现实生活中的宽带运营商类似,他们向所有消费者出售一个 100M 的带宽,远远超过用户实际使用的带宽,一个 10G 的链路可以非常轻松的服务 100 个(10G/100M)用户,但实际上宽带运行商往往会把 10G 链路用于服务 150 人或者更多,以便让链路的利用率更高,毕竟空闲在那儿也没什么意义。

Linux 内核采用的机制跟宽带运营商差不多,一般情况下都没有问题,但当大多数应用程序都消耗完自己的内存时,麻烦就来了,因为这些应用程序的内存需求加起来超出了物理内存(包括swap)的容量,内核(OOM killer)必须杀掉一些进程才能腾出空间保障系统正常运行。就如同上面的例子中,如果 150 人都占用 100M 的带宽,那么总的带宽肯定超过了 10G 这条链路能承受的范围。

示例

当你在 Linux 上运行如下代码:

public static void main(String[] args){ List l = new java.util.ArrayList(); for (int i = 10000; i < 100000; i++) { try { l.add(new int[100000000]); } catch (Throwable t) { t.printStackTrace(); } } }

在Linux的系统日志中/var/log/kern.log会出现以下日志:

Jun 4 07:41:59 plumbr kernel: [70667120.897649] Out of memory: Kill process 29957 (java) score 366 or sacrifice child Jun 4 07:41:59 plumbr kernel: [70667120.897701] Killed process 29957 (java) total-vm:2532680kB, anon-rss:1416508kB, file-rss:0kB

注意:你可能需要调整交换文件和堆大小,否则你将很快见到熟悉的Java heap space异常。在原作者的测试用例中,使用-Xmx2g指定的 2G 堆,并具有以下交换配置:

# 注意:原作者使用,由于我手里并没有 Linux 环境,所以并未测试 swapoff -a dd if=/dev/zero of=swapfile bs=1024 count=655360 mkswap swapfile swapon swapfile解决方案

解决这个问题最有效也是最直接的方法就是升级内存,其他方法诸如:调整 OOM Killer 配置、水平扩展应用,将内存的负载分摊到若干小实例上,我们不建议的做法是增加交换空间,具体原因已经在前文说过。参考资料 ② 中详细的介绍了怎样微调 OOM Killer 配置以及 OOM Killer 选择进程算法的实现,建议你参考阅读。

参考资料:

① 想要了解更多 PermGen 与 Metaspace 的内容推荐你阅读:

Java 8会解决PermGen OutOfMemoryError问题吗?Java PermGen 去哪里了?

② 如果你对 OOM Killer 感兴趣的话,强烈建议你阅读这篇文章:

理解和配置 Linux 下的 OOM Killer


【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有