为什么当堆还有20%的空间时我会收到OutOfMemory错误?

18
我已将最大堆设置为8 GB。当我的程序开始使用约6.4 GB(在VisualVM中报告)时,垃圾回收器开始占用大部分CPU资源,并且在进行约100 MB的分配时,程序会因OutOfMemory而崩溃。我正在Windows上使用Oracle Java 1.7.0_21。
我的问题是是否有GC选项可以帮助解决这个问题。我没有传递任何参数,只使用了"-Xmx8g"。
我猜测堆正在变得过于碎片化,但是GC不应该可以进行压缩吗?

你的虚拟机主机有这么多内存吗?例如:你的主机虚拟机(PC)有4GB的内存,但你给客户虚拟机10GB的内存。我不知道是否会出现分页,但我只是好奇你的主机系统规格。 - Kevin Meredith
我的机器有10GB的RAM加上10GB虚拟内存。 - Aleksandr Dubinsky
5个回答

5
收集信息碎片(这很难,因为官方文档相当糟糕),我已经确定了两个可能原因......一般有两个原因会导致这种情况发生,都与自由空间的碎片化有关(即,自由空间存在于小块中,以至于无法分配大对象)。首先,垃圾回收器可能不进行压缩,也就是说它不对内存进行碎片整理。即使是进行压缩的垃圾回收器,也可能无法完美地进行压缩。其次,垃圾回收器通常将内存区域分成保留不同类型对象的区域,并且它可能不会考虑从具有空闲内存的区域中获取空闲内存并提供给需要它的区域。
CMS垃圾收集器不进行压缩,而其他垃圾收集器(串行、并行、并行旧版和G1)都进行。Java 8中的默认收集器是ParallelOld。
所有垃圾收集器都将内存分成区域,并且据我所知,它们都懒得尝试非常努力地防止OOM错误。命令行选项-XX:+PrintGCDetails对于某些收集器非常有帮助,可以显示区域的大小以及它们具有多少自由空间。
可以尝试使用不同的垃圾收集器和调优选项。关于我的问题,启用JVM标志-XX:+UseG1GCG1收集器解决了我遇到的问题。然而,这基本上是偶然的(在其他情况下,它会更快地OOM)。一些收集器(串行、cms和G1)有广泛的调优选项,可选择各个区域的大小,使您浪费时间试图徒劳地解决问题。

最终,真正的解决方案相当不愉快。第一,是安装更多的RAM。第二,是使用较小的数组。第三,是使用ByteBuffer.allocateDirect。直接字节缓冲区(以及它们的int/float/double包装器)是类似于数组的对象,具有类似于数组的性能,它们在操作系统的本地堆上分配。操作系统堆使用CPU的虚拟内存硬件,免受碎片化问题的影响,甚至可以有效地使用磁盘的交换空间(允许您分配比可用RAM更多的内存)。然而,一个很大的缺点是JVM并不真正知道何时应该释放直接缓冲区,这使得这个选项对于长期存在的对象更加理想。最后,可能是最好的,也是最不愉快的选择是通过JNI调用本地分配和释放内存,并通过在ByteBuffer中包装它来在Java中使用它。


发人深省的帖子。 - Ravindra babu
我要补充的是,如果你利用Netty的ByteBuf,最后一个选项会容易得多。 - Aleksandr Dubinsky

2
每当这个问题出现时,实际的空闲内存都比显示的要低得多。在OutOfMemoryError发生时,您可以打印出可用内存的数量。
try {
    byte[] array = new byte[largeMemorySize];

} catch(OutOfMemroyError e) {
    System.out.printf("Failed to allocate %,d bytes, free memory= %,d%n", 
        largeMemorySize, Runtime.getRuntime().freeMemory());
    throw e;
}

内存对你来说是如何“免费”出现的?潜在可用内存的数量实际上不是 freeMemory(),而是 Runtime.getRuntime().maxMemory() - Runtime.getRuntime().totalMemory + Runtime.getRuntime().freeMemory(),但是在我的回答中,我提到了更多减少内存的因素。 - Aleksandr Dubinsky
@AleksandrDubinsky,freeMemory()并不是指空闲内存,除非你已经达到了最大内存大小,这在OOME上很可能发生。尽管如此,这并不是完全可靠的。例如,如果你请求的内存超过了堆大小,你可能根本不会触发GC。 - Peter Lawrey

2
你正在使用哪种垃圾收集器?CMS 不进行任何压缩。尝试使用新的G1 垃圾收集器 - 这会进行一些压缩。
为了更好地理解:G1 垃圾收集器或“Garbage First”收集器将堆分成块,并在标记所有垃圾后通过将所有存活的位复制到不同的块中来清除(排除)一个块,这就是实现压缩的方法。
要使用,请包括选项-XX:+UseG1GC 这个提供了关于G1和Java中垃圾收集的很好的解释。

我正在使用默认设置,无论它们是什么。我会试一下这个。看起来正是我需要的。唯一的问题是低延迟对我来说不是优先考虑的。吞吐量更重要。我是否应该仍然使用G1? - Aleksandr Dubinsky
使用G1,VisualVM报告最大堆大小等于物理内存。这是VisualVM的一个错误吗,还是G1不关心-Xmx? - Aleksandr Dubinsky
它可能默认使用CMS(它没有压缩)。G1还有其他不错的特性,所以我会尝试使用它。但你也可以尝试使用并行收集器(这是在并行停顿),命令为-XX:+UseParallelOldGC -这会进行压缩。 - selig
我不确定关于-Xmx的问题,我会看一下。 - selig

0

很可能,您正在尝试分配大量连续的内存,但所有可用的空闲内存都零散地分布在各个位置。此外,当垃圾收集器开始占用所有处理时间时,这意味着它目前正在尝试找到整个对象集中可能可以释放的1或2个对象。在这种情况下,我认为您唯一能做的就是努力将对象拆分,使其不需要那么多连续的内存(至少不是同时需要)。

编辑:据我所知,没有办法让Java打包内存,以便您可以使用全部8 GB,因为这将涉及垃圾收集器必须暂停所有其他线程,移动它们的对象,更新所有对这些对象的引用,然后刷新过期的缓存条目等等...这是非常昂贵的操作。

请参阅内存碎片化


许多Java(和其他)垃圾收集器都进行压缩(我已经发现了)。但是你是对的,这是一项非常昂贵的操作,所有线程都必须暂停(这被称为STW或Stop The World)。但由于这是一个非交互式程序,暂停不是问题。 - Aleksandr Dubinsky

0

-Xmx只确保堆大小不超过8GB,但不能保证实际分配这么多内存。你的机器只有8GB内存吗?


我的计算机有10GB,再加上另外10GB的虚拟内存。堆已经分配了8GB。 - Aleksandr Dubinsky
2
这太接近了。Java Heap 不同于 Java 最大内存消耗,它会更多。现代计算机对内存的使用比较“慷慨”,将这么多的机器资源用于堆很可能太多了。永远不要(绝对不要)让你的 JVM 进行交换。你会讨厌自己、家人、同事和他们的家人。一个进行交换的 JVM 是灾难性的。不要这样做。甚至不要让它靠近。垃圾回收和交换不兼容。 - Will Hartung

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