代码之家  ›  专栏  ›  技术社区  ›  DaveJohnston

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

  •  13
  • DaveJohnston  · 技术社区  · 15 年前

    我正在开发一个在Windows移动设备上运行的Java应用程序。为了实现这一点,我们一直在使用esmertec jbed jvm,这并不完美,但我们现在仍然坚持使用它。最近我们收到了客户关于内存不足的投诉。在玩了很多东西之后,我发现这个设备有足够的空闲内存(大约4MB)。

    OutOfMemoryErrors总是发生在代码中的同一点上,也就是在扩展StringBuffer以便向其追加一些字符时。在这个区域添加了一些日志之后,我发现我的StringBuffer中有大约29万个字符,容量约为29.05万。内部字符数组的扩展策略只是将大小增加一倍,因此它将尝试分配大约580000个字符的数组。我也打印出了这次的内存使用情况,发现它使用了大约3.8MB的总容量约为6.8MB(尽管我曾看到总可用内存有时会增加到12MB左右,所以有足够的扩展空间)。所以现在应用程序报告了一个内存不足的错误,考虑到还有多少可用内存,这个错误没有多大意义。

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

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

    所以我的问题是:是否有人有这个JVM的经验,并且能够最终确认或反驳我关于内存分配的假设?另外,对于如何修改数组的分配以使内存不被分割,有人有什么想法吗(假设我的假设是正确的)?

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

    • 我增加了StringBuffer的初始数组大小,并增加了解析器的读取大小,这样它就不需要创建这么多的数组。
    • 我改变了StringBuffer的扩展策略,这样一旦它达到某个大小阈值,它将只扩展25%,而不是100%。

    做这两件事都有一点帮助,但是当我增加进入的XML数据的大小时,我仍然会以一个相当小的大小(大约350KB)从内存中得到错误。

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

    编辑:

    另一件我刚刚尝试过的有点帮助的事情是,我将XMS设置为10M,这样就克服了JVM不在应该的时候扩展堆的问题,并且允许我在错误发生之前处理更多的数据。

    6 回复  |  直到 15 年前
        1
  •  2
  •   superfav    15 年前

    也许你可以试试 VTD 光。它似乎比SAX内存效率高。(我知道这是一个巨大的变化。)

        2
  •  2
  •   DaveJohnston    15 年前

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

    这是一个让现有客户满意的快速解决方案的一个小技巧,但是我们现在正在寻找一个不同的JVM,如果该JVM能更好地处理这个scneario,我将用一个更新报告回来。

        3
  •  1
  •   Oak    15 年前

    根据我对JVM的了解,碎片化永远不会成为问题 解决。如果没有更多的分配空间(无论是否由于碎片),垃圾收集器应该运行,并且GCS通常也会压缩数据以解决碎片问题。

    强调——你只会出现“记忆不足”的错误。 之后 GC已运行,但仍无法释放足够的内存。

    相反,我会尝试为您正在运行的特定JVM挖掘更多的选项。例如,“复制”垃圾收集器一次只使用可用内存的一半,因此将VM更改为使用其他内容可能会释放一半的内存。

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

        4
  •  0
  •   Community CDub    9 年前

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

        5
  •  0
  •   Dan Breslau    15 年前

    我不确定这些StringBuffers是否在minml中被分配——如果是这样,我假设您拥有它的源代码?如果这样做,那么在扫描字符串时,如果字符串达到某个长度(比如10000个字节),您可以提前确定字符串的确切长度,并重新分配缓冲区到该大小。这很难看,但可以节省记忆。(它甚至可能比不做lookaheads更快,因为你可能会节省 许多重新分配。)

    如果你 不要 有权访问minml源代码,那么我不确定StringBuffer的生存期与XML文档的关系是什么。但是这个建议(尽管比上一个更糟糕)可能仍然有效:既然您是从磁盘获取XML,也许您可以使用(比如)SAX解析器对其进行预解析,仅仅是为了获得字符串字段的大小,并相应地分配StingBuffers?

        6
  •  0
  •   dparnas    15 年前

    您能从设备中获得堆转储吗?

    如果获得堆转储,并且它是兼容格式的,一些Java内存分析器会给出关于相邻内存块大小的信息。我记得在IBM堆分析器中看到了这个功能 http://www.alphaworks.ibm.com/tech/heapanalyzer ,但也要检查最新的Eclipse内存分析器 http://www.eclipse.org/mat/

    如果您有可能修改XML文件,那么这可能是最快的方法。Java中的XML解析始终是内存密集型的,对于单个字段来说,300 K是相当多的。相反,您可以尝试将此字段分隔为单独的非XML文件。