4

以下 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)。

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

4

1 回答 1

6

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

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

尽管看起来好像您正在将一个转换Stream为另一个并丢弃旧的,但事实并非如此。新的Stream仍然保留对旧的参考。这是因为 resultStream不应该急切地计算底层流的所有元素,而是按需计算。以此为例:

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

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

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

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

这引出了一个问题:为什么它可以与它一起使用toList它非常简单。List.map()热切地创造新的List。时期。前一个不再被引用且符合 GC 条件。

于 2013-02-15T15:10:14.627 回答