意外的Scala集合内存行为

时间:2013-02-15 14:46:29

标签: scala memory-management collections stream

以下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,则内存使用量稳定(给予或采用GC)。

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

1 个答案:

答案 0 :(得分:6)

以下是发生的事情。 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不应急切地计算底层流的所有元素,而是按需执行。以此为例:

size

您可以根据需要链接任意数量的操作,但只读取第一行文件。每个后续操作只包装前一个foreach,保留对子流的引用。当您要求println()a.size时,评估就会开始。

返回您的代码。在第二次迭代中,您创建第三个流,保持对第二个流的引用,该引用依次保留对您最初定义的引用的引用。基本上你有一堆相当大的物体在成长。

但这并不能解释为什么内存泄漏如此之快。关键部分是...... StreamStream。没有打印(因此评估整个toListList.map()仍然是“未评估的”。未评估的流不会缓存任何值,因此它非常小。由于彼此之间的流链不断增长,内存仍然会泄漏,但速度要慢得多。

这引出了一个问题:为什么它适用于List这很简单。 {{1}}急切地创建新的{{1}}。期。之前的一个不再被引用并且有资格获得GC。