7

我会用一个 Scala 示例来问这个问题,但这很可能会影响其他允许混合命令式和函数式样式的语言。

这是一个简短的示例(UPDATED,见下文):

def method: Iterator[Int] {
    // construct some large intermediate value
    val huge = (1 to 1000000).toList        
    val small = List.fill(5)(scala.util.Random.nextInt)
    // accidentally use huge in a literal
    small.iterator filterNot ( huge contains _ )    
}

现在iterator.filterNot懒惰地工作,这很棒!因此,我们希望返回的迭代器不会消耗太多内存(实际上是 O(1))。然而,遗憾的是,我们犯了一个可怕的错误:因为filterNot它是惰性的,所以它保持对函数字面量的引用huge contains _

因此,虽然我们认为该方法在运行时需要大量内存,并且可以在方法终止后立即释放该内存,但实际上内存被卡住了,直到我们忘记返回的Iterator.

(我就是犯了这么一个错误,追了好久!看heap dumps就可以抓到这样的东西……)

避免此问题的最佳做法是什么?

似乎唯一的解决方案是仔细检查在作用域结束后仍然存在并捕获中间变量的函数文字。如果您正在构建一个非严格的集合并计划返回它,这会有点尴尬。谁能想到一些很好的技巧,Scala 特定的或其他的,可以避免这个问题,让我写出漂亮的代码?

更新:我之前给出的例子很愚蠢,正如下面 huynhjl 的回答所示。曾经是:

def method: Iterator[Int] {
    val huge = (1 to 1000000).toList // construct some large intermediate value
    val n = huge.last                // do some calculation based on it
    (1 to n).iterator map (_ + 1)    // return some small value 
}

事实上,现在我对这些事情的运作方式有了更好的了解,我并不那么担心!

4

1 回答 1

5

你确定你没有过度简化测试用例吗?这是我运行的:

object Clos {
  def method: Iterator[Int] = {
    val huge = (1 to 2000000).toList
    val n = huge.last
    (1 to n).iterator map (_ + 1)
  }

  def gc() { println("GC!!"); Runtime.getRuntime.gc }

  def main(args:Array[String]) {
    val list = List(method, method, method)
    list.foreach(m => println(m.next))
    gc()
    list.foreach(m => println(m.next))
    list.foreach(m => println(m.next))
  }
}

如果我理解正确,因为main即使在gc()调用之后使用迭代器,JVM 也会保留huge对象。

这就是我运行它的方式:

JAVA_OPTS="-verbose:gc" scala -cp classes Clos

这是它最后打印的内容:

[Full GC 57077K->57077K(60916K), 0.3340941 secs]
[Full GC 60852K->60851K(65088K), 0.3653304 secs]
2
2
2
GC!!
[Full GC 62959K->247K(65088K), 0.0610994 secs]
3
3
3
4
4
4

huge所以在我看来,这些物品好像被回收了......

于 2010-10-18T06:22:39.130 回答