为什么Scalaz 7枚举器会泄漏内存?

为什么Scalaz 7枚举器会泄漏内存?,scala,memory-leaks,scalaz,iterate,scalaz7,Scala,Memory Leaks,Scalaz,Iterate,Scalaz7,以下定义导致内存泄漏: def enumIterator1[E, F[_]: Monad](x: => Iterator[E]) : EnumeratorT[E, F] = new EnumeratorT[E, F] { def apply[A] = (s: StepT[E, F, A]) => { def go(xs: Iterator[E]): IterateeT[E, F, A] = if(xs.isEmpty) s.pointI

以下定义导致内存泄漏:

def enumIterator1[E, F[_]: Monad](x: => Iterator[E]) : EnumeratorT[E, F] =
  new EnumeratorT[E, F] {
    def apply[A] = (s: StepT[E, F, A]) => {
      def go(xs: Iterator[E]): IterateeT[E, F, A] =
        if(xs.isEmpty) s.pointI
        else {
          val next = xs.next
          s mapCont { k => 
            k(Iteratee.elInput(next)) >>== enumIterator1[E, F](xs).apply[A] 
          }
        }
      go(x)
    }
  }
可通过以下测试观察泄漏:

(Iteratee.fold[Array[Byte], IO, Long](0L)(_+_.length) 
  &= enumIterator1(
    Iterator.continually(
      Array.fill(1 << 16)(0.toByte)).take(1 << 16))
).run.unsafePerformIO
为什么?


我有一个模糊的概念,即解释与闭包的引用模式有关,但我不能提出这种行为的具体原因。我正在试图追踪,我怀疑(希望?)了解此泄漏可能有助于确定该泄漏的原因。

问题是传递给
mapCont
的匿名函数关闭了
下一个
。反过来,这被传递给enumIterator的惰性变量所覆盖,它被由enumIterator1
形成的新的
枚举数
所覆盖,它被
apply
中的匿名函数所覆盖,最后通过传递给下一次迭代的
mapCont
的匿名函数关闭

因此,通过一系列闭包,每个枚举数都比它的前一个枚举数关闭。无论是否捕获了
next
,都可能会发生这种情况,因此无论哪种方式,您都会有轻微的内存泄漏。但是,您最终会在其中一个闭包中捕获
next
,这意味着迭代器生成的每个值都会保留在内存中,直到整个过程完成(这些值占用了大量内存)

通过在传递给
mapCont
的匿名函数中移动
next
next
不再被我们的闭包链捕获,因此主存泄漏消失(尽管闭包之间仍然相互关闭,这可能是一个问题)

解决这个问题的最好办法可能是简化它。正如Brian Kernighan的名言:

每个人都知道调试的难度是编写程序的两倍。因此,如果您在编写它时尽可能聪明,您将如何调试它

我不确定我是否完全理解代码,但我怀疑以下内容是等效的:

def enumIterator1[E, F[_]: Monad](x: => Iterator[E]) : EnumeratorT[E, F] =
  new EnumeratorT[E, F] {
    def apply[A] = {
      val xs = x
      def innerApply(s: StepT[E, F, A]): IterateeT[E, F, A] = {
        if(xs.isEmpty) s.pointI
        else {
          val next = xs.next
          s mapCont { cont => // renamed k to cont, as the function, rather than the variable, is k
            cont(Iteratee.elInput(next)) >>== innerApply
          }
        }
      }
      innerApply
    }
  }
您还可以从使事情更加明确中获益。例如,如果您定义了一个具有顶级作用域的命名类,并显式传入它所需的任何内容,而不是隐式关闭其作用域内任何内容的匿名枚举数,该怎么办

我使用
-XX:+HeapDumpOnOutOfMemoryError
、VisualVM和
javap
来查找问题的原因。它们应该是你所需要的一切

更新 我想我开始摸索代码应该做什么,我已经相应地更新了代码。我认为问题在于如何使用enumIterator1[E,F](xs).apply[A]
。代码创建了一个新的
enumerator
,只是为了得到它的apply方法,但是创建了一个by-name变量,并在这个过程中关闭了所有的东西和它的狗。由于
xs
的值不会从一个递归变为下一个递归,因此我们创建了一个
innerApply
方法,该方法关闭val
xs
,并重新使用
innerApply

更新2 我很好奇,所以我查看了Scalaz源代码,看看他们是如何解决这个问题的。下面是一些与Scalaz本身类似的代码:

def enumIterator[E, F[_]](x: => Iterator[E])(implicit MO: MonadPartialOrder[F, IO]) : EnumeratorT[E, F] =
  new EnumeratorT[E, F] {
    import MO._ // Remove this line, and you can copy and paste it into your code
    def apply[A] = {
      def go(xs: Iterator[E])(s: StepT[E, F, A]): IterateeT[E, F, A] =
        if(xs.isEmpty) s.pointI
        else {
          s mapCont { k => 
            val next = xs.next
            k(elInput(next)) >>== go(xs)
          }
        }
      go(x)
    }
  }

他们使用currying而不是closure来捕获
xs
,但这仍然是一种“内部应用”的方法。

我正试图遵循闭包链,我无法理解第二步。为什么
mapCont
的匿名函数参数会被
enumIterator1
的by-name参数关闭?(我想你指的是
enumIterator 1
而不是
enumIterator
)我可能犯了一个错误,但IIRC这是因为它通过
outer$
查找
xs
。我想这是有道理的,但看起来一定是编译器的错误。闭包不应该包含它们从未使用过的引用。谢谢你找到这个。我计划在接下来的几天内核实你的解释,然后奖励奖励。顺便说一句,建议的实施并不等同。为了在函数代码中使用
enumIterator1
,迭代器必须是一个按名称参数。与迭代器不同,枚举器是可重用的,因此正确使用
enumIterator1
需要传递一个构造迭代器的函数,而不是传递迭代器本身。我怀疑我的代码在某种微妙的方面有所不同-我与迭代器的经验是从Play开始的,这有一些微妙的差异。您可能也应该检查一下我在闭包链上的工作——我的方法基本上是遵循堆转储中的引用,并通过在
javap
中查看相应的类来尝试找出它们的作用。但不管怎样,简化都会让事情变得更清楚。
def enumIterator[E, F[_]](x: => Iterator[E])(implicit MO: MonadPartialOrder[F, IO]) : EnumeratorT[E, F] =
  new EnumeratorT[E, F] {
    import MO._ // Remove this line, and you can copy and paste it into your code
    def apply[A] = {
      def go(xs: Iterator[E])(s: StepT[E, F, A]): IterateeT[E, F, A] =
        if(xs.isEmpty) s.pointI
        else {
          s mapCont { k => 
            val next = xs.next
            k(elInput(next)) >>== go(xs)
          }
        }
      go(x)
    }
  }