【问题标题】:What is the optimal hashmap capacity/load factor for 5-10M entries?5-10M 条目的最佳哈希图容量/负载因子是多少?
【发布时间】:2016-09-23 09:26:10
【问题描述】:

HashMap 中有大约 5-10M 条目,我无法更改代码结构。我正在运行java-Xms=512m -Xmx=1024mHashMap 构造函数中避免java.lang.OutOfMemoryError: GC overhead limit exceeded 的最佳容量/负载因子值是多少?

private final Map<String, ReportResultView> aggregatedMap = new HashMap<>(????, ????);

【问题讨论】:

  • 通过调整这些参数,您无能为力。您可以尝试避免调整地图大小,但这意味着您必须为最坏的情况(您可能需要的最大条目数)预先调整大小。改变负载系数是不值得的。
  • 如果你有10M的条目,最好的容量是10M,不过不用担心。一旦你添加了所有 10M 条目,它具有(至少)该容量,这是不可避免的。您传递给构造函数的值是初始容量,它主要影响初始插入性能,当太低时,仅此而已。一旦添加了这些值,它就不再有任何影响了。
  • @holger 它会影响调整大小期间发生的内存使用峰值。还要注意糟糕的 API,其中“容量”不是在调整大小之前可以插入的条目数,而是存储桶数组的原始大小。您必须除以负载因子才能获得对用户有意义的大小。
  • @Marko Topolnik:比这更复杂。如果您指定 10M,由于四舍五入到 2 的下一个幂,您实际上将获得 >16M 的容量,因此在任何负载因子 >0.6(默认值为 0.75)的情况下,您可以放置​​ 10M 条目而无需调整大小。请注意,这不是我们所说的“糟糕的 API”,而是“糟糕的实现”。
  • 这取决于你的目标。如果您指定 10M 作为初始容量,但只放置 5M 条目,那么您显然是在浪费空间。如果您指定 5M 作为初始容量,但放入 10M 条目,那么您将在中间进行一次重新哈希操作。因此,最大性能是您的目标,您可能希望避免重新哈希操作,但如果更有可能拥有 5M-6M 条目并且您希望减少平均内存占用,您可能会接受在某些情况下可能发生的一次重新哈希操作。

标签: java dictionary optimization hashmap java-8


【解决方案1】:

总结: 在这种情况下,负载因子可能看起来很有趣,但它不能成为 OOME 的根本原因,因为负载因子仅控制浪费的后备数组空间,并且默认情况下(负载因子为 0.75)这只消耗大约 2.5% 的堆(并且不会导致高对象计数 GC 压力)。更有可能的是,您存储的对象及其关联的 HashMap.Entry 对象使用的空间已经消耗了堆。

详情: HashMap 的负载因子控制映射使用的底层引用数组的大小。较小的负载因子意味着给定大小的空数组元素较少。因此,一般来说,增加负载因子会减少内存使用量,因为空数组插槽更少。3

然而,这已经确定,您不太可能通过调整负载因子来解决您的 OOME。然而,一个空的数组元素只会“浪费”4 个字节1。因此,对于 5M-10M 元素的数组,0.75(默认值)的负载因子将浪费大约 25 MB 的内存2

这只是您分配的 1,024 MB 堆内存的一小部分,因此您将无法通过调整负载因子来解决 OOME(除非您使用非常愚蠢的东西,例如极低的负载系数为 0.05 或其他)。默认负载系数就可以了。

很可能是对象的实际大小和存储在HashMap 中的对象Entrys 导致了问题。每个映射都有一个HashMap.Entry 对象,该对象包含键/值对和几个其他字段(例如哈希码和链接时指向下一项的指针)。这个 Entry 对象本身 consumes about 32 bytes - 当添加到底层数组条目的 4 个字节时,这就是 单独条目的开销的堆的 40 bytes * 10M entries = 400M。然后,您存储的实际对象也会占用空间:如果您的对象甚至有少量字段,它们将至少与 Entry 对象一样大,并且您的堆几乎耗尽。

您收到GC limit exceeded 错误而不是heap alloc failed 的事实通常意味着您正在缓慢接近堆限制,搅动大量对象:在这种情况下,GC 在运行之前往往会以这种方式失败空间不足。

因此很可能您只需要为应用程序分配更多堆,找到存储更少元素的方法,或减小每个元素的大小(例如,使用不同的数据结构或对象表示)。


[1] 在 HotSpot 上通常是 4 个字节,即使在运行 64 位 JDK 时也是如此 - 尽管在某些 64 位平台上它可能是 8 个字节 如果 compressed oops 由于某种原因被禁用.

[2] 最坏的情况,0.75 负载因子意味着调整大小后负载为 0.75 / 2 = 0.375,因此您有 (1 - 0.375) * 10,000,000 空元素,每个元素 4 个字节 = ~25 MB。在重新哈希期间,您可以添加另一个 1.5 左右的因子,在最坏的情况下,因为新旧后备数组将同时在堆上。但是,当地图大小稳定时,这并不适用。

[3] 即使使用链接也是如此,因为通常使用链接不会增加内存使用量(即,Entry 元素已经嵌入了“下一个”指针,无论该元素是否在链中或不)。 Java 8 使事情变得复杂,因为改进了 HashMap 实现,使得大型链可以转换为树,这可能会增加占用空间。

【讨论】:

    【解决方案2】:

    避免java.lang.OutOfMemoryError: GC overhead limit exceeded

    当 hashmap 调整大小时,它需要重新分配内部表。因此,您需要为您的 VM 提供足够的内存来处理临时副本或预先调整 hashmap 的大小,以防止发生大小调整。

    您还可以查看 https://github.com/boundary/high-scale-lib 中的 hashmap 实现,它应该提供较少破坏性的调整大小行为。

    【讨论】:

    • ArrayList 不同,调整 HashMap 的大小不会对内存使用产生很大的暂时影响,因为仅调整了后备数组的大小。 Entry 对象被简单地移动了。 Entry 对象(甚至不包括存储的底层键或值对象)比支持数组大一个数量级,因此调整大小峰值并不高(不像 ArrayList 支持数组是支持数组的 100℅开销)。
    猜你喜欢
    • 2011-10-30
    • 1970-01-01
    • 2019-02-05
    • 1970-01-01
    • 1970-01-01
    • 2016-10-20
    • 2023-04-08
    • 2011-11-14
    • 1970-01-01
    相关资源
    最近更新 更多