在 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 中分配大量数组时避免内存碎片的主要内容,如果未能解决你的问题,请参考以下文章