为什么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
方法,该方法关闭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)
}
}
他们使用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)
}
}