【问题标题】:Out of memory while adding elements in immutable set in Scala在 Scala 中添加不可变集合中的元素时内存不足
【发布时间】:2018-11-19 14:51:32
【问题描述】:

当我在不可变集合中添加元素时,我会在循环中耗尽内存。集合中已经有很多对象,我想它会消耗大量内存。我知道在不可变集合中添加元素时,Scala 会首先将现有集合复制到新集合中,然后将元素添加到新集合中并返回这个新集合。

所以假设我的 JVM 内存是 500mb,而这个集合消耗了 400mb。现在,在添加新元素之前,Scala 尝试将旧集复制到新集(我认为这将再次消耗 400mb)现在在这一步,它已经超过了 JVM 内存(总消耗内存 800),因此它会抛出内存不足错误。 代码看起来有点像下面

private def getNewCollection(myMuttableSet:Set[MyType]): Set[MyType] = {
myMuttableSet.flatMap(c => {
      val returnedSet = doSomeCalculationsAndreturnASet // this method returns a large collection so duing the loop the collection grows exponentially 
      if (returnedSet.isEmpty) Set.empty[MyType]
      else doSomeCalculationsAndreturnASet + MyType(constArg1,constArg2)  (I have case class of MyType)     
    })
}

如果我的理解正确,请告知。

【问题讨论】:

  • 能否在问题中包含循环代码?
  • 你的理解是正确的
  • 是的,你是对的,但很高兴看到你的代码,因为如果你正在做类似的事情:var set = Set.empty[Int]; for (i <- 1 to 1000) { set = set + i},那么,恕我直言,最好只使用@987654321 @.
  • mutable valimmutable var 之间的偏好应该部分取决于可见性。请参阅stackoverflow.com/a/11386867/3226045 了解有关引用透明度的说明。
  • 添加了程序中使用的算法

标签: scala collections garbage-collection out-of-memory


【解决方案1】:

它并不那么简单,因为它取决于Set 中元素的大小。

创建一个新的Set 是一个 操作并且不会复制集合中的元素,它只是创建一个指向相同对象的新包装器(通常是某种哈希表) .

如果您有一小组大型对象,则复制该组可能不会占用太多存储空间,因为这些对象将在两组之间共享。大部分内存由集合中的对象使用,不需要复制这些内存来创建新集合。因此,您的 400Mb 可能会变为 450Mb 并符合内存限制。

如果您有大量小对象,则复制该集合可能会使存储空间增加一倍。大部分内存用于Set 本身,无法在原始集和副本之间共享。在这种情况下,您的 400Mb 很容易接近 800Mb。

既然你的内存用完了,你说有很多对象,那么听起来这就是问题所在,但我们需要查看代码才能确定。

【讨论】:

  • 感谢 tim 的回答。我正在检查代码并会回来
【解决方案2】:

现在,在添加新元素之前,Scala 会在这一步尝试将旧集合复制到新集合中(我认为这会再次消耗 400mb),

这是不正确的。

scala 中的不可变集合(包括Sets)被实现为persistent data structures,它通常有一个称为“结构共享”的属性。这意味着,当结构更新时,它并没有完全复制,而是大部分被重复使用,只有相对较小的部分实际上是从头开始重新创建的。

最简单的例子是List,它被实现为一个单链表,根指向头部。

例如,您有以下代码:

val a = List(3,2,1)
val b = 4 :: a
val c = 5 :: b

虽然这三个列表加起来总共有 3 + 4 + 5 = 12 个元素,但它们在物理上共享节点,并且只有 5 个List 节点。

5 →  4  →  3 →  2  → 1
↑    ↑     ↑
c    b     a   

类似的原则也适用于Set。 scala 中的Set 实现为HashTrie。我不会详细介绍Trie 的细节,只需将其视为具有高分支因子的树即可。现在,当该树更新时,它并没有完全复制。仅复制从树根到新/更新节点的路径中的节点。

对于HashTrie,树的深度不能超过7级。因此,在 scala 中更新 Set 时,您正在查看与 O(7 * 32) 成比例的内存分配(最多 7 级,每个节点粗略地说是一个 32 的数组),无论 Set 大小如何。


查看您的代码,您的内存中有以下内容:

  1. myMuttableSet 一直存在,直到 getNewCollection 返回
  2. myMuttableSet.flatMap 在下面创建可变缓冲区。此外,flatMap 完成后,buffer.result 会将可变缓冲区的内容复制到不可变集。所以实际上存在两个集合的短暂时刻。
  3. flatMap 的每一步,returnedSet 也会保留内存。

旁注:如果您已经将结果缓存在returnedSet 中,为什么还要再次调用doSomeCalculationsAndreturnASet?会不会是问题的根源?

因此,在任何给定的时间点,您都在内存中(以较大者为准):

  • myMuttableSet + mutable result set buffer + returnedSet + (another?) result doSomeCalculationsAndreturnASet
  • myMuttableSet + mutable result set buffer + immutable result set

总之,无论您的记忆问题是什么,将元素添加到 Set 很可能不是罪魁祸首。我的建议是暂停您在调试器中的程序并使用任何分析器(例如 VisualVM)在不同阶段进行堆转储。

【讨论】:

  • 感谢您的详细分析。我会检查并回来。
猜你喜欢
  • 2012-09-06
  • 1970-01-01
  • 2012-01-07
  • 1970-01-01
  • 2017-10-10
  • 1970-01-01
  • 2022-12-18
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多