插入50000个对象到HashMap时为什么会出现OutOfMemoryError?

14

我正在尝试将大约50,000个对象(因此有50,000个键)插入到java.util.HashMap<java.awt.Point,Segment>中。但是,我一直收到OutOfMemory异常。(Segment是我的自定义类 - 很轻量级 - 一个String字段和3个int字段。)

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    at java.util.HashMap.resize(HashMap.java:508)
    at java.util.HashMap.addEntry(HashMap.java:799)
    at java.util.HashMap.put(HashMap.java:431)
    at bus.tools.UpdateMap.putSegment(UpdateMap.java:168)

这看起来相当荒谬,因为我发现机器上有足够的内存 - 包括可用的RAM和虚拟内存的HD空间。

可能Java运行时带着一些严格的内存要求吗? 我能否增加这些要求?

HashMap是否存在某些奇怪的限制?我需要实现自己的类吗?还有其他值得看的类吗?

(我在2GB RAM的Intel机器上,在OS X 10.5下运行Java 5。)

10个回答

22

您可以通过向Java传递 -Xmx128m (其中128是以兆字节为单位的数量)来增加最大堆大小。我不记得默认大小是多少,但我想它是相当小的。

您可以使用Runtime类以编程方式检查可用内存有多少。

// Get current size of heap in bytes
long heapSize = Runtime.getRuntime().totalMemory();

// Get maximum size of heap in bytes. The heap cannot grow beyond this size.
// Any attempt will result in an OutOfMemoryException.
long heapMaxSize = Runtime.getRuntime().maxMemory();

// Get amount of free memory within the heap in bytes. This size will increase
// after garbage collection and decrease as new objects are created.
long heapFreeSize = Runtime.getRuntime().freeMemory();

(来自Java开发者年鉴的示例)

这个问题在Java HotSpot VM常见问题解答Java 6 GC调优页中也有部分讨论。


我该如何确定当前的大小,以便将来知道?谢谢! - Frank Krueger
我必须同意 Allain 的观点--2048 MB 似乎有点过多。你可能需要使用分析器来查看所有分配的来源。 - Michael Myers
客户端虚拟机的默认大小为64m。 - Brandon DuRette
在Windows上,2048甚至无法启动虚拟机。在32位的Windows上,最大可用内存约为1.4G,具体取决于加载了哪些其他dll文件。在OSX上,就像原帖中所说,如果将MX参数设为最大内存,则虚拟机可能会启动或不会启动。 - John Gardner
1
当然,我可以使用分析器并优化哈希函数以减少内存使用,但是这个工具每月只运行一两次。我的时间最好花在优化产品上,而不是支持工具上。但还是谢谢你的建议! - Frank Krueger
显示剩余3条评论

7

有些人建议更改HashMap的参数以紧缩内存需求。我建议量而不猜;可能是其他原因导致OOME。特别地,我建议使用NetBeans Profiler或者VisualVM(Java 6自带但你卡在了Java 5)。


4
如果您事先知道对象的数量,另一个可以尝试的方法是使用HashMap(int capacity,double loadfactor)构造函数,而不是默认的no-arg构造函数,该构造函数使用默认值(16,0.75)。如果HashMap中的元素数量超过(capacity * loadfactor),那么在HashMap中底层的数组将被调整到下一个2的幂,并且表将被重新散列。这个数组还需要一个连续的内存区域,例如,如果您从32768大小的数组扩大到65536大小的数组,您需要有一个256kB的空闲内存块。为了避免额外的分配和重新散列惩罚,只需从开始就使用更大的哈希表。这也会减少您没有足够大的连续内存区域来适应映射的机会。

3
实现通常使用数组来支持。数组是固定大小的内存块。hashmap实现从给定容量(比如100个对象)的数组中存储数据开始。
如果它填满了数组,而你继续添加对象,地图需要暗中增加其数组大小。由于数组是固定的,它通过在内存中创建一个完全新的、略微更大的数组,以及当前数组来完成这一操作。这被称为扩展数组。然后将所有项目从旧数组复制到新数组中,并取消引用旧数组,希望在某个时刻可以对其进行垃圾回收并释放内存。
通常,将项目复制到较大的数组中以增加映射容量的代码是这种问题的原因。有"愚蠢"的实现和聪明的实现,它们使用增长或负载因子来确定基于旧数组大小的新数组大小。有些实现隐藏了这些参数,有些则没有,所以您不能总是设置它们。问题在于,当您无法设置时,它会选择一些默认的负载因子,例如2。因此,新数组的大小是旧数组的两倍。现在,您所谓的50k映射具有100k的后备数组。
看看是否可以将负载因子降低到0.25或其他值。这会导致更多的哈希映射冲突,这会损害性能,但您正在击中内存瓶颈,需要这样做。
使用此构造函数:(http://java.sun.com/javase/6/docs/api/java/util/HashMap.html#HashMap(int, float))

2

当启动Java时,您可能需要设置标志-Xmx512m或更大的数字。我认为默认值是64mb。

补充说明: 在使用分析器确定对象实际使用的内存量后,您可能需要查看弱引用或软引用,以确保在不再使用它们时,不会意外地将某些内存托管给垃圾收集器。


1

1

这些答案中隐含的是Java具有固定的内存大小,并且不会超出配置的最大堆大小。这与C语言不同,C语言的内存大小仅受其运行的机器限制。


@Frank Krueger:这个选择是为了实现更高效的垃圾回收器。固定的最大大小有助于优化这个过程。 - Mnementh

1

默认情况下,JVM使用有限的堆空间。该限制取决于JVM实现,并且不清楚您正在使用哪个JVM。在Windows以外的操作系统上,具有2 Gb或更多内存的32位Sun JVM将使用默认最大堆大小为物理内存的1/4,或在您的情况下为512 Mb。然而,“客户端”模式JVM的默认值仅为64 Mb的最大堆大小,这可能是您遇到的问题。其他供应商的JVM可能会选择不同的默认值。

当然,您可以使用-Xmx<NN>m选项显式指定堆限制给java,其中<NN>是堆的兆字节数。

粗略估计,您的哈希表应该只使用约16 Mb,因此必须在堆上有一些其他大型对象。如果您可以在TreeMap中使用Comparable键,则可以节省一些内存。

请参阅"5.0 JVM中的人体工程学"以获取更多详细信息。


提高限制已经奏效,但非常感谢您提供TreeMap的参考。 - Frank Krueger

1

Java堆空间默认是有限的,但这听起来仍然很极端(不过你的50000个段有多大?)

我怀疑你有其他问题,比如集合中的数组变得太大,因为所有东西都被分配到同一个“插槽”中(当然也会影响性能)。然而,如果你的点是均匀分布的,这似乎不太可能。

我想知道为什么你使用HashMap而不是TreeMap?即使点是二维的,你也可以用一个比较函数来子类化它们,然后进行log(n)查找。


1
随机想法:与HashMap相关的哈希桶并不特别内存高效。您可能想尝试使用TreeMap作为替代方案,看看它是否仍然提供足够的性能。

有趣,你能详细解释一下吗,Kevin? - James McMahon

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