以下 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)。我知道流会懒惰地评估,但一旦生成就保留元素。但似乎在我上面的代码中,每个新流(由最后一行代码生成)都以某种方式保留了对先前流的引用。有人可以帮忙解释一下吗?
最佳答案
这是发生的事情。 Stream
总是被懒惰地评估,但已经计算的元素被“缓存”以备后用。懒惰的评估是至关重要的。看这段代码:
a = a.flatMap( v => Some( v ) )
尽管看起来好像您正在将一个
Stream
转换为另一个并丢弃旧的,但事实并非如此。新的 Stream
仍然保留对旧的引用。那是因为 result 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
。没有打印(因此评估整个 Stream
) Stream
仍然“未评估”。未评估的流不缓存任何值,所以它非常 slim 。由于彼此之间不断增长的流链,内存仍然会泄漏,但速度要慢得多。这就引出了一个问题:为什么它可以与
toList
一起使用,这很简单。 List.map()
急切地创建新的 List
。时期。前一个不再被引用并且符合 GC 的条件。关于scala - 意外的 Scala 集合内存行为,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/14897173/