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

13

我正在开发一个Java应用程序,该应用程序在Windows Mobile设备上运行。为了实现这一点,我们一直在使用Esmertec JBed JVM,尽管它不完美,但我们暂时无法更改。最近,我们收到了客户有关OutOfMemoryErrors的投诉。经过多次试验,我发现设备有足够的可用内存(约4MB)。

OutOfMemoryErrors总是发生在代码的同一点上,即在扩展StringBuffer以追加一些字符时。在此区域周围添加一些日志后,我发现我的StringBuffer大约有290000个字符,容量约为290500。内部字符数组的扩展策略仅是将其大小加倍,因此它将尝试分配大约580000个字符的数组。同时,我还打印出了此时的内存使用情况,并发现它使用的约为6.8MB中的3.8MB(尽管我有时看到可用总内存上升到约12MB,因此还有很大的扩展空间)。因此,在这一点上,应用程序报告了OutOfMemoryError,考虑到还有这么多可用内存,这显然不合理。

我开始思考应用程序在此之前的操作。基本上是我正在使用MinML(一个小型XML Sax解析器)解析XML文件。 XML中的一个字段有大约300k个字符。解析器从磁盘流式传输数据,并默认每次仅加载256个字符。因此,当它到达涉及该字段的位置时,解析器将调用处理程序的“characters()”方法超过1000次。每次它将创建一个新的char []来容纳256个字符。处理程序只需将这些字符附加到StringBuffer中。 StringBuffer的默认初始大小仅为12,因此随着字符附加到缓冲区中,它将不得不多次增长(每次都会创建一个新的char [])。

我的假设是,虽然之前的char[]数组可以被垃圾收集,因此有足够的空闲内存,但也有可能没有足够大的连续内存块来适应我尝试分配的新数组。也许JVM并不聪明,不能扩展堆大小,因为它认为没有必要,显然有足够的空闲内存。

所以我的问题是:是否有人有关于这个JVM的经验,并且能够确定地证实或否定我的内存分配假设?另外,如果我的假设是正确的,是否有任何想法来改进数组的分配,使得内存不会变得碎片化?

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

  • 增加StringBuffer的初始数组大小和增加解析器的读取大小,以便不需要创建太多的数组。
  • 更改StringBuffer的扩展策略,使其达到一定大小阈值后,只扩展25%而不是100%。

这两个方法都有所帮助,但当我增加输入的xml数据大小时,仍然会在相当低的大小(约350kb)处遇到OutOfMemoryErrors。

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

编辑:

我刚刚尝试了另一件事情,这有点帮助,那就是我将Xms设置为10M。因此,这解决了JVM应该扩展堆时未扩展堆的问题,并允许我在出现错误之前处理更多的数据。

6个回答

2
也许你可以尝试使用VTD轻量级解析器。它似乎比SAX更节省内存。(我知道这是一个巨大的变化。)

2

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

这是一个快速解决方案的折衷办法,以保持现有客户的满意度,但我们现在正在寻找另一个JVM,如果那个JVM能更好地处理这种情况,我会回来报告更新。


1
根据我对JVM的了解,碎片化不应该是一个必须解决的问题。如果没有足够的内存分配(无论是由于碎片化还是其他原因),垃圾收集器应该运行,并且GC通常会压缩数据以解决碎片化问题。
强调一下 - 只有在运行GC后仍然无法释放足够的内存时,才会出现“内存不足”的错误。
我建议更深入地研究您正在运行的特定JVM的选项。例如,“复制”垃圾收集器一次只使用可用内存的一半,因此更改VM以使用其他内容可能会释放一半内存。
我并不是真正建议您的VM使用简单的复制GC,我只是建议在VM级别上进行探测。

很不幸,我使用的JVM支持几乎不存在(除非有人知道一个好的地方可以获取Esmertec JBed CDC的支持?)。你知道如何更改GC选项的标准命令行选项吗? - DaveJohnston
@DaveJohnston:你可以查看流行JVM的文档,希望你的JVM表现相同;但是Java VM规范没有定义标准(事实上,它明确指出:“运行时数据区的内存布局,垃圾收集算法等由实现者自行决定”)。 - Oak

0

你确定吗?那篇文章讲述了如何使对象更容易被垃圾回收。 - Dan Breslau
我没有创建任何引用对象吗?正如我所说,我认为对象没有被垃圾回收并不是我的问题,因为JVM报告有足够的空闲内存。问题在于空闲内存在哪里?它是否碎片化?这就是为什么JVM无法分配我的新数组的原因吗? - DaveJohnston

0

我不确定这些StringBuffers是否是在MinML内分配的 - 如果是这样的话,我假设您有它的源代码?如果您有源代码,那么在扫描字符串时,如果字符串达到一定长度(比如10000个字节),您可以向前查找以确定字符串的确切长度,并重新分配一个相应大小的缓冲区。这很丑陋,但可以节省内存。(甚至可能比不进行lookaheads更快,因为您潜在地节省了许多重新分配。)

如果您没有访问MinML源代码,则我不确定StringBuffer的生命周期与XML文档的关系。但是,这个建议(尽管比上一个建议更丑陋)仍然可能有效:由于您从磁盘中获取XML,因此可以使用(例如)SAX解析器预解析它,仅获取字符串字段的大小并相应地分配StingBuffers?


StringBuffers被分配在SaxParser的Handler对象中(在这种情况下是MinML)。因此,相关的处理程序分配一个StringBuffer,然后每次调用characters()方法时都会将更多数据附加到它上面。我不是在扫描字符串,而是从文件流式传输所有内容,因此我无法提前找出最终字符串的大小,除非我按照您在第二个建议中所说的那样对文件进行两次解析。但正如您所说,这很丑陋且耗时。 - DaveJohnston
丑陋,没错。但是它可能比你想象的要快,特别是如果你当前的方法需要大量重新分配内存的话。 - Dan Breslau

0

你能从设备中获取堆转储吗?

如果你得到了堆转储并且它是兼容的格式,一些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 正常工作的方法。 - DaveJohnston

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