【问题标题】:Unexpected Scala collections memory behaviour意外的 Scala 集合内存行为
【发布时间】:2025-04-16 12:15:01
【问题描述】:

以下 Scala 代码(在 2.9.2 上):

var a = ( 0 until 100000 ).toStream
for ( i <- 0 until 100000 )
{
    val memTot = Runtime.getRuntime().totalMemory().toDouble / ( 1024.0 * 1024.0 )
    println( i, a.size, memTot )

    a = a.map(identity)
}

在循环的每次迭代中使用越来越多的内存。如果a定义为( 0 until 100000 ).toList,那么内存使用是稳定的(give or take GC)。

我了解流会延迟评估,但会在生成元素后保留元素。但似乎在我上面的代码中,每个新流(由最后一行代码生成)以某种方式保留对先前流的引用。有人可以帮忙解释一下吗?

【问题讨论】:

  • 它仍然使用a = a.map(identity) 泄漏内存,你确定它适合你吗?
  • @TomaszNurkiewicz 你是对的。它仍然会随着身份线泄漏。感谢您的现场 - 我会更新问题。

标签: scala memory-management collections stream


【解决方案1】:

这就是发生的事情。 Stream 总是被延迟评估,但已经计算的元素被“缓存”以备后用。惰性评估至关重要。看这段代码:

a = a.flatMap( v => Some( v ) )

虽然看起来好像您正在将一个Stream 转换为另一个并丢弃旧的,但这并不是发生的情况。新的Stream 仍然保留对旧的引用。这是因为结果Stream 不应该急切地计算底层流的所有元素,而是按需计算。以此为例:

io.Source.fromFile("very-large.file").getLines().toStream.
  map(_.trim).
  filter(_.contains("X")).
  map(_.substring(0, 10)).
  map(_.toUpperCase)

您可以根据需要链接任意数量的操作,但读取第一行时几乎不会触及文件。每个后续操作只是包装前一个Stream,持有对子流的引用。当您要求sizeforeach 时,评估就开始了。

回到您的代码。在第二次迭代中,您创建第三个流,保存对第二个流的引用,而后者又保留对您最初定义的流的引用。基本上你有一堆相当大的对象在增长。

但这并不能解释为什么内存泄漏如此之快。关键部分是...println(),或者准确地说是a.size。不打印(因此评估整个 StreamStream 仍然“未评估”。未评估的流不缓存任何值,因此非常苗条。内存仍然会由于流的不断增长而泄漏,但速度要慢得多。

这引出了一个问题:为什么它与toList 一起工作这很简单。 List.map() 急切地创建新的List。时期。前一个不再被引用且符合 GC 条件。

【讨论】:

  • 确实是一个非常好的解释。我有一种感觉,它是这样的。有趣的是,出于内存消耗的原因,我一直对流保持警惕。但是我有以下(简化的)代码(0 until 100).sliding(10).toSeq,当我(也许)期待Vector[Vector[Int]]时,它出乎意料地(至少对我来说)给了我Stream[Vector[Int]]。然后这给了我最初的记忆问题。问题中的代码是我将其最小化为的。
最近更新 更多