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

时间:2013-10-09 22:17:07

标签: 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
        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

然而,一个小的改变(即移动xs.next调用)会阻止泄漏:

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 (moved down)
          s mapCont { k => 
            val next = xs.next
            k(Iteratee.elInput(next)) >>== enumIterator1[E, F](xs).apply[A] 
          }
        }
      go(x)
    }
  }

为什么吗

我有一个模糊的概念,即解释与闭包的参考模式有关,但我不能想出这种行为的具体原因。我正在尝试追踪a different memory leak,我怀疑(希望?)理解此泄漏可能有助于确定该漏洞的原因。

1 个答案:

答案 0 :(得分:3)

问题是传递给mapCont的匿名函数会在next之后关闭。反过来,这是由我们传递给enumIterator的惰性变量关闭的,enumIterator由Enumerator形成的新enumIterator1关闭,applymapCont中的匿名函数关闭。 1}},最后由传递给next的匿名函数关闭以进行下一次迭代。

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

通过移动mapCont传递给next的匿名函数,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 } } 不再被我们的闭包链捕获,因此主内存泄漏消失(尽管你的闭包仍然关闭)彼此之间,这可能是一个问题)。

解决此问题的最佳方法可能是简化它。正如Brian Kernighan所说:

  

每个人都知道调试的难度是首先编写程序的两倍。所以,如果你在写作时就像你一样聪明,你将如何调试呢?

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

EnumeratorT

您也可以从更明确的事情中受益。例如,如果不是让匿名-XX:+HeapDumpOnOutOfMemoryError隐式关闭​​其范围内所需的任何内容,那么您可以定义一个具有顶级范围的命名类,并传递它所需的任何内容。

我使用javap,VisualVM和enumIterator1[E, F](xs).apply[A]来查找问题的原因。它们应该是你需要的一切。

更新

我开始弄清楚代码应该做什么,并且我已经相应地更新了我的代码。我认为问题在于使用EnumeratorT。代码创建了一个新的xs只是为了获得它的apply方法,但创建了一个名字变量并在这个过程中关闭了所有和它的狗。由于innerApply的值不会从一次递归更改为下一次递归,因此我们创建一个xs方法,该方法关闭val innerApply,然后重新使用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) } }

更新2

我很好奇,所以我在Scalaz源代码中查看它们是如何解决这个问题的。这里有一些与Scalaz本身类似的代码:

xs

他们使用currying而不是闭合来捕获{{1}},但它仍然是一种“内在应用”方法。