内存不足错误:Java堆大小可用内存时

10

我使用命令java -Xmx240g mypackage.myClass运行Java程序。

操作系统是Ubuntu 12.10。

top命令显示MiB Mem 245743 total,并且从一开始就显示Java进程virt 254g,其中res不断增加直到达到169g。此时程序只有单线程,CPU%大多数时间都为100%,在某个点会跳到1300-2000(这是多线程垃圾回收器),然后res缓慢移动到172g。此时Java出现以下错误并崩溃:

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

出错的位置是new double[2000][5]这一行。

java -version命令输出结果:

java version "1.7.0_15" OpenJDK Runtime Environment (IcedTea7 2.3.7) (7u15-2.3.7-0ubuntu1~12.10) OpenJDK 64-Bit Server VM (build 23.7-b01, mixed mode)

硬件是Amazon cr1.8xlarge实例。

看起来即使有大量内存可用,Java程序仍然会崩溃。这显然是不可能的,我必须误解了某些数字。我应该去哪里查找并理解出现问题的原因?

编辑:

我没有指定任何GC选项。唯一的命令行选项是-Xmx240g

我的程序在许多输入上都能正常工作,并且top命令有时显示它使用了高达98.3%的内存。但是我使用特定的程序输入重现了上述情况。

编辑2:

这是一款科学应用程序。它有一个巨大的树(1-10百万个节点),每个节点都有一对大小约为300x3-900x5的double数组。在初始树创建后,程序并不分配太多内存。大部分时间都会对这些数组进行一些算术运算。

编辑3:

HotSpot JVM也以相同的方式崩溃了,在170-172g时使用了大量CPU,并以相同的错误崩溃。看起来70-75%的内存是JVM不想越过的某条神奇线。

最终解决方案:使用-XX:+UseConcMarkSweepGC -XX:NewRatio=12,程序通过了170g标记并愉快地继续工作。


1
在IcedTea上增加240g的堆空间,这需要勇气。 - jonathan.cone
乔纳森·科恩更新了问题。 - user1766873
使用jvisualvm(或其他可用工具)查找内存泄漏的位置。可能新的double [2000] [5]并不会引起问题。 - sarahTheButterFly
可能相关:https://dev59.com/z3I-5IYBdhLWcg3wPF12 - MarianP
我会尝试使用非Oracle JVM。 - MarianP
显示剩余5条评论
3个回答

9

分析

首先,您需要获取一个堆转储文件,以便在JVM崩溃时确定堆的确切状态。请将以下一组标志添加到命令行中:

-XX:+HeapDumpOnOutOfMemoryError -verbose:gc -XX:+PrintGCDetails

当程序崩溃时,JVM会将堆写入磁盘。对于这样大小的堆来说,这将需要很长时间。如果您已经在运行Eclipse,可以下载Eclipse MAT或安装该插件。从那里,您可以加载堆转储并运行一些预设报告。您需要检查泄漏嫌疑和支配树,以确定内存使用情况,并确定是否存在实际泄漏。

此后,我建议您阅读Oracle关于垃圾回收的文档,但以下是您可以考虑的一些事项:

并发GC

-XX:+UseConcMarkSweepGC 

我从未听说过有人在如此大的堆上使用并行收集器而没有问题。您可以激活并发收集器,并且需要了解增量模式并确定它是否适合您的工作负载/硬件组合。

堆空闲比率

-XX:MinHeapFreeRatio=25

将此值调低以在进行完整收集时降低垃圾收集器的门槛。这可能会防止在进行完整收集时内存不足。40%是默认值,请尝试较小的值进行实验。

新比率

-XX:NewRatio

我们需要了解更多关于你实际工作量的信息:这是一个webapp吗?还是swing app?对象在堆上预计存活的时间越长,将对新比率值产生影响。像你正在运行的服务器模式VM默认情况下具有相当高的新比率(8:1),如果你有很多长期存在的对象,这可能不理想。

非常感谢您详细的回答。我更新了问题,我确实有很多长生命周期的对象。 - user1766873
请澄清一下,MinHeapFreeRatio 如何帮助我?如果 JVM 现在没有足够的内存进行完整的垃圾回收,为什么更少的内存就足够了呢? - user1766873
比率决定垃圾回收后堆空间必须有多少百分比是空闲的。如果这个数字太高,你会更快地耗尽堆空间,因为收集器无法清理足够的内存以满足所需的比率。这将在进行完整GC期间耗尽堆空间时发挥作用。 - jonathan.cone

1
作为一般建议,永远不要使用OpenJDK,尤其是在生产环境中,它比Sun/Oracle的JDK慢得多。
除此之外,我从未见过VM使用如此多的内存,但我猜这就是您需要的(或者您的代码使用了比所需更多的内存?)
编辑:对于服务器来说,OpenJDK很好,与Sun/Oracle JDK的唯一区别在于桌面相关的内容(声音、GUI等),因此请忽略该部分。

谢谢,我会尝试Sun JVM。是的,我需要大量的内存。 - user1766873
1
关于内存,考虑一下你是否真的需要这么多。不要误解我,我相信你使用70GB有自己的理由,但是根据我的经验,有时候从另一个角度重新审视问题可以节省很多时间、内存和痛苦。也许你可以尝试使用不同的算法或数据结构? - Juan Antonio Gomez Moriano
请停止散布关于OpenJDK速度较慢或其他方面的FUD。这可能在3到4年前是真的。对于服务器Java,OpenJDK 7与Oracle JDK 7相同。 - Denis Tulskiy
@DenisTulskiy,这不是我的经验,但我很乐意发现我错了,你能否提供一个好的来源来支持你的说法? - Juan Antonio Gomez Moriano
1
根据这篇文章:http://weblogs.java.net/blog/robogeek/archive/2007/10/openjdk_encumbr.html,OpenJDK 中重新实现的部分包括字体渲染、Java2D 和 JavaSound,因此主要是桌面相关的内容。至于加密方面的内容,我认为他们开源了现有的代码。此外,OpenJDK 7 现在是 JDK7 的参考实现。因此对于服务器代码,HotSpot 实现和类库是相同的。 - Denis Tulskiy

1
如果我正确理解你的问题,似乎内存泄漏实际上是在程序到达new double[2000][5]这一行之前发生的。当程序到达这一行时,似乎内存已经很少了,因此当这一行请求更多内存时就会抛出异常。
我会使用jvisualvm或类似的工具来查找内存泄漏的位置。我遇到的内存泄漏大多与在循环中创建字符串、未清除缓存等有关。

对于在循环中创建的字符串,+1很常见,这些字符串连接是非常典型的。 - Juan Antonio Gomez Moriano

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接