在Scala的不可变集中添加元素时内存不足

时间:2018-11-19 14:51:32

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

在不可变集中添加元素时,我会在循环中耗尽内存。集合中已经有很多对象,我想它正在消耗大量内存。我知道,在不可变集合中添加元素时,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)     
    })
}

请告知我的理解是否正确。

2 个答案:

答案 0 :(得分:0)

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

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

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

如果您有大量的小对象,那么复制该对象可能会使存储量增加一倍。大多数内存用于Set本身,并且不能在原始集和副本之间共享。在这种情况下,您的400Mb可能很容易接近800Mb。

由于您的内存不足,并且您说有很多对象,所以听起来这是问题所在,但是我们需要查看代码以确保确定。

答案 1 :(得分:0)

  

现在,在添加新元素之前,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还会保留内存。

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

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

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

总而言之,无论您的内存问题是什么,将元素添加到Set中的可能性都不大。我的建议是在调试器中暂停程序,并使用任何探查器(例如VisualVM)在不同阶段进行堆转储。