Java中的HashMap,一亿个条目

34
我希望将1亿个词语及其频率(存储在文本数据库中)存储到一个 HashMap <String, Double> 中。然而,程序报告了“内存不足”错误。我尝试增加堆大小到 -Xmx15000M,但是程序会运行半小时后再次抛出相同的异常。从中读取单词和频率的文件大小为1.7GB。
非常感谢任何的帮助。
谢谢 :-)

39
你到底在做什么需要一亿个术语?你在为谷歌工作吗? - DJClayworth
你为什么要首先将它存储在HashMap中呢?正如许多人建议的那样,你可以将其存储在数据库中,或者你可能想要进行映射减少操作(Hadoop?)。虽然这完全取决于为什么使用HashMap。 - ch4nd4n
1
有多少个不同的术语?如果有很多重复,那么数据量太大,内存无法承受,但频率表仍然可以是合理大小。在这种情况下,只是需要分阶段处理完整个文件的问题... - mikera
1
使用数据库。这是Java HashMap性能优化/替代方法的副本。 - BalusC
Sun/Oracle的HashMap默认实现是基于节点的结构。这可能是你内存不足的原因之一。如果你创建一个不带节点的不同类型的实现,你可以轻松地将所有数据装入内存。一个简单的想法:使用巨大的排序数组和二分查找来查找键。 - kevinarpe
显示剩余6条评论
15个回答

18

对于这样的文字处理,通常使用树而不是哈希表来处理,如果你可以忍受更长的查找时间。该结构对自然语言而言具有相当高的内存效率,其中许多单词具有共同的起始字符串。

根据输入的情况,一棵Patricia树可能会更好。

(此外,如果这确实是自然语言中的单词,请确保您真的需要1亿个条目吗?常用单词的数量令人惊讶地低,商业解决方案(词预测、拼写矫正)很少使用超过100,000个单词,无论哪种语言。)


我尝试了Patricia trie。这一次我撞到了GC限制,15GB的内存仍然不够。 :-) - ablimit
5
它指向了我其他的解决方案,并让我学会了一个新的库工具。由于所有答案都指出了有用的东西,所以很难选择最好的答案。尽管只有一个人能获得最佳答案,但我非常感谢所有认真回答问题的人。希望我可以选择多个最佳答案... - ablimit
1
斯洛伐克语有一百万个单词,因为我们经常使用屈折变化。 - Oliv

11

你的问题在于1.7 GB原始文本即使不考虑字符串对象的开销,也超过了1500 MB。对于大型映射,你应该使用数据库或基于文件的Map,它们会使用磁盘内存而不是堆内存。

更新

我认为为堆分配15 GB的内存对于大多数jvm来说是不可能的。它不适用于任何32位的jvm,我也不认为64位的jvm会工作。 当足够的RAM可用时,64位jvm应该可以使用15 GB的内存。


@nos 这将取决于是否达到了3.4 GB的大小。 - josefx
你可以将数据库放入内存中以加快速度,但是,使用数据库会更加典型和常见。 - Dean J
4
我知道这篇文章已经有些旧了,但是你可以给单个JVM进程分配超过15G的RAM。我已经尝试过25GB并且它可以工作。硬件配置要求:拥有64核心和64GB RAM的机器,并使用Sun JDK 6。 - Sanjay T. Sharma
@Sanjay T. Sharma 很好知道,我没有64位系统的访问权限,所以我无法检查它,并错误地假设堆大小会受到系统或JVM限制的限制。 - josefx
你不需要在内存中存储原始文本。你只需要存储独特的术语,这应该是数百万倍少的数据,以及相应的整数。根据输入数据,我们可能会谈论100,000个术语,或者最多1百万个,这在现代计算机中很容易存储。 - Jay Askren

6
一个1.7 GB的文件相对较小,可以在RAM中处理和存储。我处理更大的文件并将它们存储在内存中而没有问题。可以使用数据库,但可能过于复杂或完美取决于您打算如何使用数据。
正如其他人所说,对于自然语言,唯一值的数量可能相对较小,因此映射实际上不会变得那么大。我不会使用java.util.HashMap,因为它在存储原始类型(例如int)等基本值时非常低效。java.util.HashMap将原始类型存储为对象。它还将每个值存储在HashMap.Entry对象中,这会浪费内存。由于这两个因素,java.util.HashMap使用的内存比Trove、Fastutil等替代方案多得多。

如上所述,有几种地图实现没有这些问题。由于您将数字存储在地图中,因此额外的好处是,当您将新值放入地图或更新旧值时,无需不断在对象和原始类型之间切换(即装箱/拆箱),因此您将获得性能提升。可以在{{link3:Java性能调整指南上的此帖子}}中找到适用于大量数据的各种原始哈希映射的基准:


5

如果你有一亿个术语,那么几乎肯定超出了内存存储的限制。将术语存储在某种数据库中,可以使用商业数据库,也可以编写允许您访问文件以获取所需信息的程序。如果您拥有的文件格式不允许您快速访问文件,则将其转换为可以快速访问文件的格式,例如使每个记录成为固定大小,这样您就可以立即计算任何记录编号的文件偏移量。然后对记录进行排序,就可以非常快速地进行二进制搜索。您还可以编写代码,无需将整个文件存储在内存中即可大大加快对文件的访问。


5
如果您只需要一个轻量级的KeyValue(Map)存储,我建议考虑使用Redis。它非常快速,并且可以在需要时持久化数据。唯一的缺点是需要在Linux机器上运行Redis存储。
如果您受限于Windows系统,则MongoDB是一个不错的选择,如果您可以在64位系统上运行它。

但是似乎在Java中使用Redis有点复杂? - ablimit
Redis现在也兼容Windows :) - Aman Gupta

2

您还可以尝试使用词干提取来增加重复项的数量。

例如,cat = Cats = cats = Cat

或者

swim = swimming = swims

尝试在Google上搜索“Porter Stemmer”。


1
其他答案已经指出问题在于内存使用。根据您的问题域,您可以设计一个关键类来减少整体内存占用。例如,如果您的关键字由自然语言短语组成,您可以分离并整理组成短语的单词。
public class Phrase {
  private final String[] interned;

  public Phrase(String phrase) {
    String[] tmp = phrase.split(phrase, "\\s");

    this.interned = new String[tmp.length];

    for (int i=0; i<tmp.length; ++i) {
      this.interned[i] = tmp[i].intern();
    }
  }

  public boolean equals(Object o) { /* TODO */ }
  public int hashCode() { /* TODO */ }
}

事实上,即使字符串不表示自然语言,只要存在可以在字符串之间利用的重叠部分,此解决方案也可能有效。

1

Trove THashMap使用的内存要少得多。不过,我怀疑这是否足以减小尺寸。除了严格在内存中检索之外,您需要其他地方来存储此信息。


1
请不要推荐Trove,有更好的选择:http://java-performance.info/hashmap-overview-jdk-fastutil-goldman-sachs-hppc-koloboke-trove-january-2015/ - leventov
1
@leventov,你知道这个答案已经六年了吗?当时它是一个不错的选择。更新的信息很好,但只需要说出来就可以了。 - AHungerArtist

1
放弃使用HashMap,将所有数据加载到HBase或其他NoSQL数据存储中,并使用MapReduce操作编写查询。这是Google搜索和许多其他处理大量数据的网站采用的方法。它已被证明可以扩展到基本上无限的规模。

1
说“基本上无限”有点误导:http://www.multivax.com/last_question.html - Ehtesh Choudhury

1

考虑使用cdb替换它。最多可达4 GB,且:

在大型数据库中成功查找通常只需要两次磁盘访问。失败的查找只需要一次。


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