在 Java 中分配大量数组时避免内存碎片

Posted

技术标签:

【中文标题】在 Java 中分配大量数组时避免内存碎片【英文标题】:Avoid memory fragmentation when allocating lots of arrays in Java 【发布时间】:2011-01-05 05:25:29 【问题描述】:

我正在开发一个在 Windows Mobile 设备上运行的 Java 应用程序。为了实现这一点,我们一直在使用 Esmertec JBed JVM,它并不完美,但我们现在坚持使用它。最近我们收到了客户关于 OutOfMemoryErrors 的投诉。在玩了很多东西后,我发现该设备有足够的可用内存(大约 4MB)。

OutOfMemoryErrors 总是发生在代码中的同一点,即在扩展 StringBuffer 以向其附加一些字符时。在这个区域周围添加了一些日志后,我发现我的 StringBuffer 中有大约 290000 个字符,容量大约为 290500。内部字符数组的扩展策略只是将大小加倍,所以它会尝试分配一个数组大约 580000 个字符。我也打印了这段时间的内存使用情况,发现它使用了大约 3.8MB,总共大约 6.8MB(尽管我看到总可用内存有时会上升到大约 12MB,所以有足够的扩展空间)。因此,此时应用程序报告了 OutOfMemoryError,考虑到还有多少可用空间,这没有多大意义。

到目前为止,我开始考虑应用程序的操作。基本上正在发生的事情是我正在使用 MinML(一个小型​​ XML Sax 解析器)解析一个 XML 文件。 XML 中的一个字段包含大约 300k 个字符。解析器从磁盘流式传输数据,默认情况下一次只加载 256 个字符。因此,当它到达有问题的字段时,解析器将调用处理程序的“characters()”方法超过 1000 次。每次它将创建一个包含 256 个字符的新 char[]。处理程序只是将这些字符附加到 StringBuffer。 StringBuffer 的默认初始大小仅为 12,因此当字符被附加到缓冲区时,它将不得不增长多次(每次创建一个新的 char[])。

我的假设是,虽然有足够的空闲内存,因为之前的 char[] 可以被垃圾收集,但可能没有足够大的连续内存块来容纳我试图分配的新数组.并且可能 JVM 不够聪明,无法扩展堆大小,因为它很愚蠢,认为没有必要,因为显然有足够的空闲内存。

所以我的问题是:是否有人对此 JVM 有任何经验,并且可能能够最终证实或反驳我对内存分配的假设?另外,有没有人有任何想法(假设我的假设是正确的)关于如何改进数组的分配以使内存不会变得碎片化?

注意:我已经尝试过的事情:

我增加了 StringBuffer 的初始数组大小,并增加了解析器的读取大小,因此它不需要创建这么多数组。 我更改了 StringBuffer 的扩展策略,使其在达到一定大小阈值后仅扩展 25% 而不是 100%。

做这两件事有点帮助,但是当我增加输入的 xml 数据的大小时,我仍然会在相当小的大小(大约 350kb)处得到 OutOfMemoryErrors。

要补充一点:所有这些测试都是在使用相关 JVM 的设备上执行的。如果我使用 Java SE 1.2 JVM 在桌面上运行相同的代码,我没有任何问题,或者至少在我的数据大小达到大约 4MB 之前我不会遇到问题。

编辑:

我刚刚尝试过的另一件事是我将 Xms 设置为 10M。所以这解决了 JVM 没有在应该扩展堆的时候扩展堆的问题,并允许我在错误发生之前处理更多数据。

【问题讨论】:

【参考方案1】:

也许你可以试试VTD light。它似乎比 SAX 更节省内存。 (我知道这是一个巨大的变化。)

【讨论】:

【参考方案2】:

只是为了更新我自己的问题,我发现最好的解决方案是设置最小堆大小(我将其设置为 10M)。这意味着 JVM 永远不必决定是否扩展堆,因此即使它应该有足够的空间,它也永远不会(到目前为止在测试中)死于 OutOfMemoryError。到目前为止,在测试中,我们已经能够将我们解析的数据量增加三倍而不会出现错误,如果我们真的需要,我们可能会走得更远。

这是一个让现有客户满意的快速解决方案的小技巧,但我们现在正在研究一个不同的 JVM,如果该 JVM 能更好地处理这个场景,我会报告更新。

【讨论】:

【参考方案3】:

据我对 JVM 的了解,碎片绝不应该是您必须解决的问题。如果没有更多的分配空间 - 无论是否由于碎片 - 垃圾收集器都应该运行,并且 GC 通常还会压缩数据以解决碎片问题。

强调一下 - 您只会在 运行 GC 之后出现“内存不足”错误,但仍然无法释放足够的内存。

我会尝试更深入地研究您正在运行的特定 JVM 的选项。例如,“复制”垃圾收集器一次只使用一半可用内存,因此将 VM 更改为使用其他东西可能会释放一半内存。

我并不是真的建议您的 VM 使用简单的复制 GC,我只是建议在 VM 级别上进行探索。

【讨论】:

不幸的是,对我正在使用的 JVM 的支持几乎不存在(除非有人知道获得对 Esmertec JBed CDC 支持的好地方??)。知道用于更改 GC 选项的标准命令行选项是什么吗? @DaveJohnston:您可以查看流行 JVM 的文档并希望您的行为相同;但是 Java VM 规范没有定义标准(实际上,它明确表示:“运行时数据区域的内存布局,使用的垃圾收集算法 [...] 由实现者自行决定” )。【参考方案4】:

我认为你有足够的内存,但正在创建大量的引用对象。试试这篇文章:https://web.archive.org/web/1/http://articles.techrepublic%2ecom%2ecom/5100-10878_11-1049545.html?tag=rbxccnbtr1 了解更多信息。

【讨论】:

你确定吗?那篇文章讨论了如何使对象更容易进行垃圾收集。 我没有创建任何引用对象??正如我所说,我不认为对象没有被垃圾收集有问题,因为 JVM 报告有大量可用内存。问题是空闲内存在哪里?是碎片化的吗?这就是 JVM 无法分配我的新数组的原因吗?【参考方案5】:

我不确定这些 StringBuffers 是否在 MinML 中分配——如果是这样,我假设你有它的来源?如果你这样做了,那么也许当你正在扫描一个字符串时,如果字符串达到一定的长度(比如 10000 字节),你可以提前确定字符串的确切长度,然后重新分配一个缓冲区到那个大小.这很难看,但它会节省内存。 (它甚至可能比不进行前瞻更快,因为您可能会节省 许多重新分配。)

如果您没有可以访问 MinML 源,那么我不确定 StringBuffer 的生命周期与 XML 文档的关系。但是这个建议(尽管它比上一个更难看)可能仍然有效:由于您是从磁盘获取 XML,也许您可​​以使用(例如)SAX 解析器预先解析它,只是为了获取字符串的大小字段,并相应地分配 StingBuffers?

【讨论】:

StringBuffers 是在 SaxParser(在本例中为 MinML)的 Handler 对象中分配的。因此,有问题的处理程序分配一个 StringBuffer ,然后每次调用 characters() 方法时都会附加更多数据。我不是在扫描字符串,它都是从文件中流式传输的,所以我无法提前找出最终字符串的大小,除非我按照你在第二个建议中所说的那样对文件进行两次解析。但正如你所说,这既丑陋又耗时。 丑陋,是的。但它可能比您预期的要快,尤其是在您当前的方法需要大量重新分配的情况下。【参考方案6】:

您能否从设备中获取堆转储?

如果您获得堆转储并且它的格式兼容,一些 Java 内存分析器会提供有关连续内存块大小的信息。我记得在 IBM Heap Analyzer http://www.alphaworks.ibm.com/tech/heapanalyzer 中看到过这个功能,但也可以查看更新的 Eclipse Memory Analyzer http://www.eclipse.org/mat/

如果您有可能修改 XML 文件,那可能是最快的出路。 Java 中的 XML 解析总是占用大量内存,对于单个字段来说 300K 是相当多的。相反,您可以尝试将此字段分离到一个单独的非 xml 文件中。

【讨论】:

我非常怀疑我是否能够获得堆转储,JVM 在你可以用它做什么方面非常有限,或者至少它没有很好的文档记录,所以我不会不知道该怎么做。修改 XML 是我们将其视为最后手段的一种可能性,因为 XML 是服务器返回的一组搜索结果。更改它意味着对我们的服务器结构进行更改,纯粹是为了解决看起来像是 JVM 的问题。如果这样就好了,但希望我们能找到一种方法让 JVM 正常工作。

以上是关于在 Java 中分配大量数组时避免内存碎片的主要内容,如果未能解决你的问题,请参考以下文章

如何避免堆碎片?

JAVA里String数组在内存分配中分配的空间每个占几个字节?

面试题:Linux是如何避免内存碎片的

伙伴系统之避免碎片--Linux内存管理(十六)

查看/解决 Windows XP 内存碎片的工具

使用 io.Writer 时避免在 golang 中分配过多的内存