【问题标题】:Memory Efficient way to Handle a Large HashMap处理大型 HashMap 的内存高效方法
【发布时间】:2016-07-06 19:14:18
【问题描述】:

我有一个项目正在处理正在写入 Excel 文件的大量数据。我将这些数据以Map<List<String>, Integer> 的形式存储在一个静态HashMap 中,其中列表的大小只有3。然而,Map 中的条目数可以在0 到11,300 之间。

这个项目的流程是:

  • 用条目加载地图

  • 迭代地图并做一些事情

  • 为下一组条目清除地图

我最近发现的关于 HashMap 的一点是,当超出设定的大小时它会如何重新调整大小。因此,我的地图不仅会不断地以惊人的长度重新调整大小,而且当我清除最大的条目集时,它很可能有大约 20,000 个空条目。

所以我正在尝试对这件事进行微优化,但我陷入了如何做到这一点的两难境地。我的两个想法是:

  1. 将初始 HashMap 的默认值设置为允许它最多只调整一次大小的值

  2. 使用每个新条目的预期平均大小重新初始化 HashMap,以限制重新调整大小并允许垃圾收集器进行一些清理

我的直觉告诉我选项 2 可能是最合理的选项,但这仍然可以证明根据下一个条目集重新调整大小。但是选项一极大地限制了一次操作的重新调整大小,但随后给我留下了数以千计的空条目。

我提出的两种解决方案中的一种是否比另一种更好,两者在内存改进方面没有太大差异,还是我监督过其他一些解决方案(不涉及更改数据结构)?

编辑:仅在某些情况下,我想这样做是因为偶尔项目会耗尽堆内存,我正在尝试确定这个巨大的地图现在或可能产生的影响有多大。

EDIT2:为了澄清,地图本身的大小是较大的值。密钥大小(即列表)只有 3。

【问题讨论】:

  • 不要执行选项 2。如果您有足够的内存来处理最坏的情况,即 11300 List 对象作为映射的键,那么您就有足够的内存用于整个过程。缩小地图并没有真正获得任何收益,但是重新扩展会损失性能。与其他所有情况相比,缩小所节省的内存是最小的。这是或当然假设它是一个持续的过程。不要在不使用它的情况下长时间保留大而空的地图。在这种情况下,请移除地图并重新分配它。
  • 有什么理由不能使用 TreeMap?我认为它不会明显变慢(log_2(11300) 只有 13),并且不会浪费任何空间。
  • @Oliver 映射键是List,不是Comparable,阻止使用TreeMap。可以提供自定义的Comparator,但是您仍然必须决定列表的顺序,这可能不可行。此外,TreeMap 在峰值时比HashMap 使用更多的内存。
  • 你的记忆力有那么大吗?您的应用程序是否在极低端或嵌入式系统上运行?或者它必须能够在一个小容器内运行?地图中的 20000 个条目几乎不是现代台式机甚至智能手机上的堆用完的原因。
  • @Leon 我在本地机器上的内存并没有那么受限,但是我运行我的应用程序的服务器可能有也可能没有很多东西可以玩,这就是为什么我不只是将堆大小提高到 3GB

标签: java memory-management hashmap


【解决方案1】:

这里的问题和接受的回复非常错误,我不得不回复。

我有一个正在处理大量数据的项目 写入excel文件。我将这些数据存储在一个静态 HashMap 中 form Map, Integer>,其中列表的大小只有 3. Map 中的条目数可以在任何地方 从 0 到 11,300。

请不要误会我的意思,但这是很小!!!甚至不用费心优化这样的东西!我赶紧做了个测试,在hashmap中填充“11300”个元素不到十几毫秒。

我最近发现的关于 HashMap 的一点是,当设置大小被破坏时,它是如何重新调整大小的。因此,不仅我的地图不断地以惊人的长度重新调整大小,而且当我清除最大的一组
条目。

...只是为了清楚。空条目几乎不占用空间,这些只是空指针。在 64 位机器上每个插槽 8 个字节,或在 32 位机器上每个插槽 4 个字节。我们这里最多讨论的是几千字节。

使用每个新条目集预期的平均大小重新初始化 HashMap > 以限制重新调整大小并允许垃圾收集器进行一些清理。

这不是条目的平均“大小”,而是预期的平均条目数量。

编辑:仅在某些情况下,我想这样做是因为 偶尔项目用完堆内存,我正在尝试 确定这张巨幅地图的影响有多大。

不太可能是地图。使用分析器!您可以毫不费力地存储数百万个元素。


接受的答案是错误的

您可以在初始化时更改这些值,因此大小为 11300 并且 factorLoad 为 1,这意味着地图的大小不会增加,直到 你的最大值已经达到,在你的情况下,据我所知, 永远不会。

这不是一个好的建议。使用与预期插入的项目数相同的容量和“一”的负载因子,您肯定会遇到大量的哈希冲突。这将是一场性能灾难。


结论

如果你不知道东西是如何工作的,不要尝试微优化。

【讨论】:

  • 我在很久以前就问过这个问题,所以是的,天真占了上风。如果我没记错的话,我所工作的公司的服务器基本上已经达到最大值,无论如何我都在寻找释放这个应用程序的内存使用量,而不是杀死任何东西。尽管如此,我仍然很感激回顾我仍然可以使用的信息。您之所以被否决,可能是因为您的回答(起初)更像是一种批评,而且似乎有点不合时宜。但再读一遍,我没明白,明白你为什么回答。只是想让你知道,作为 OP,我很欣赏这些信息。
【解决方案2】:

我做了一些研究,最终访问了这个页面:How does a HashMap work in Java

倒数第二个标题与调整开销有关,说明 HashMap 的默认值是 size16factorLoad0.75

您可以在初始化时更改这些值,因此 11300size 和 1 的 factorLoad,这意味着地图在达到最大值之前不会增加大小,在您的情况下,正如我明白了,永远不会。

我做了一个快速实验,使用以下代码:

public static void main(String[] args) throws Exception {
    Map<String, Integer> map = new HashMap<>(11000000, 1);
    //        Map<String, Integer> map = new HashMap<>();
    for (int i = 0; i < 11000000; i++) {
        map.put(i + "", i);
    }
    System.out.println(map.size());
    Thread.sleep(9000);
}

交换两个Map 初始化,然后检查它在Task Manager 中消耗的内存。

在设置了初始大小和 factorLoad 后,它使用了~1.45GB 的内存。 如果没有设置值,它将使用~1.87GB 的内存。

每次都重新初始化Map,而不是清除它以换取可能更小的Map 来代替它会更慢,但您可能会暂时获得更多内存。

你也可以两者都做。重新初始化以设置初始大小和factorLoad 属性,如果您知道每个循环的List 对象的数量。

该文章还表明,Java 8 HashMap 虽然可能更快,但也可能比 Java 7 具有更多的内存开销。可能值得尝试在两个版本中编译程序,看看哪个版本提供了改进的内存解决方案。如果没有别的,会很有趣。

【讨论】:

  • 这是一个很好的发现。巧合的是,我正在运行 Java 8,所以这绝对值得一试。我会修改你刚刚提出的两个建议,看看这会如何影响我的表现。
  • 这似乎释放了足够的内存让应用程序运行完成。我“应该”避免说谢谢,但是谢谢。这是提高内存使用率的良好开端
  • 现在这是一个非常糟糕的主意。使用与预期插入的项目数相同的上限和“一”的负载因子,您肯定会遇到大量的哈希冲突。这将是一场性能灾难。
猜你喜欢
  • 2012-11-04
  • 2011-04-27
  • 2013-08-05
  • 2011-01-29
  • 2014-03-26
  • 2012-12-29
  • 1970-01-01
  • 2019-05-07
  • 1970-01-01
相关资源
最近更新 更多