4

以下定义导致内存泄漏:

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)
    }
  }

为什么?

我有一个模糊的概念,即解释与闭包的参考模式有关,但我无法提出这种行为的具体原因。我正在尝试追踪不同的内存泄漏,并且我怀疑(希望?)了解此泄漏可能有助于确定导致该泄漏的原因。

4

1 回答 1

3

问题是传递给的匿名函数mapCont关闭了next。反过来,这是由我们传递给 enumIterator 的惰性变量封闭的,它由由Enumerator形成的新变量封闭enumIterator1,由 in 的匿名函数apply封闭,最终由传递的匿名函数封闭到mapCont下一次迭代。

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

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

解决此问题的最佳方法可能是简化它。正如布赖恩·克尼汉 (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
    }
  }

你也可以从使事情更明确中受益。例如,如果不是让匿名EnumeratorT来隐式关闭其范围内所需的任何内容,而是定义一个具有顶级范围的命名类,并显式传递它需要的任何内容。

我使用了-XX:+HeapDumpOnOutOfMemoryErrorVisualVM,并javap找到了问题的原因。它们应该是你需要的一切。

更新

我开始了解代码应该做什么,并且我已经相应地更新了我的代码。我认为问题在于使用enumIterator1[E, F](xs).apply[A]. 代码创建一个新的EnumeratorT只是为了获取它的 apply 方法,但是创建了一个按名称的变量并在这个过程中关闭了所有东西和它的狗。由于从一个递归到下一个递归的值xs不会改变,我们创建了一个innerApply方法来关闭 valxs并重新使用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)
    }
  }

他们使用柯里化而不是闭包来捕获xs,但这仍然是一种“内部应用”方法。

于 2014-06-19T15:39:30.257 回答