GC应该发生在Seq的处理元素上吗?



这是我代码的简化版本:

// Very small wrapper class for Large BigData object
class LazilyEvaluatedBigData(a: String) {
lazy val generate: BigData
}
// Contents of BigData are considered to be large
class BigData {
def process: Seq[Int]  // Short Seq in general, say 2-3 elements
}
val seq1: Seq[LazilyEvaluatedBigData]
val seq2: Seq[LazilyEvaluatedBigData]
val results1 = seq1.flatMap(_.generate.process)
val results2 = seq2.flatMap(_.generate.process)

现在 - 我希望在这里发生的是,在任何给定时间,内存中只需要一个 BigData 类实例。鉴于不需要将 seq1 或 seq2 的"处理"元素保存在内存中,我希望它们被垃圾回收 - 但是我的进程将 OOMing 保持在 flatMaps :(

我对 scala 垃圾收集器的期望是否过高。是否假定需要引用 seq1 和 seq2 的头部?


最终修复是合并此类:

class OnDemandLazilyEvaluatedBigData(a: String) {
def generate(): LazilyEvaluatedBigData = new LazilyEvaluatedBigData(a)
}

然后将 seq1 和 seq2 转换为:

val seq1: Seq[OnDemandLazilyEvaluatedBigData]

你对GC的期望不高,但你假设的是你的代码没有表达的东西。

你有一个

lazy val generate: BigData

在你的LazilyEvaluatedBigData课上,你有

val seq1: Seq[LazilyEvaluatedBigData]

在正在执行的代码中。

您的代码按预期运行,因为:

  • lazy val不是def:一旦被调用,它保证它将存储评估结果。你不应该期望它会在程序内存不足时将其值进行垃圾回收,并在再次需要时重新计算它。
  • Seq保证它不会丢失任何元素。例如,List永远不会仅仅因为程序内存不足而删除其任何元素。您需要类似具有软引用的序列之类的东西才能发生这种情况,或者您必须重写代码,以便在不再需要时不引用包含已处理元素的列表的头部。

如果您将这两点结合起来考虑,那么您的代码本质上是说,在flatMap结束时,seq1是一个序列,其中包含对多个LazilyEvaluatedBigData实例的引用,并且这些LazilyEvaluatedBigData实例中的lazy val都被评估并保存在内存中。


如果您希望在flatMap期间不再需要BigData实例时对其进行垃圾回收,只需将generate声明为

def generate: BigData

然后,您的seq1seq2将只容纳Strings 的薄包装器,并且flatMap的每一步都会加载一个BigData实例,使用process将其再次压缩成一个小Seq[Int],然后BigData实例可以再次被垃圾回收。这在没有太多内存的情况下成功运行:

// Very small wrapper class for Large BigData object
class LazilyEvaluatedBigData(a: String) {
def generate: BigData = new BigData(128)
}
// Contents of BigData are large
class BigData(m: Int) {
val data = Array.ofDim[Byte](1000000 * m)
def process: Seq[Int] = List(1,2,3)
}
val seq1: Seq[LazilyEvaluatedBigData] = List.fill(100)(new LazilyEvaluatedBigData(""))
val results1 = seq1.flatMap(_.generate.process)
println("run to end without OOM")

(它会失败lazy val)。

另一种选择是使用软引用(粗略草图,未经彻底测试):

class LazilyEvaluatedBigData(a: String) {
import scala.ref.SoftReference
private def uncachedGenerate: BigData = new BigData(128)
private var cachedBigData: Option[SoftReference[BigData]] = None
def generate: BigData = {
val resOpt = for {
softRef <- cachedBigData
bd <- softRef.get
} yield bd
if (resOpt.isEmpty) {
val res = uncachedGenerate
cachedBigData = Some(new SoftReference(res))
res
} else {
resOpt.get
}
}
}
class BigData(m: Int) {
val data = Array.ofDim[Byte](1000000 * m)
def process: Seq[Int] = List(1,2,3)
}
val seq1: Seq[LazilyEvaluatedBigData] = List.fill(100)(new LazilyEvaluatedBigData(""))
val results1 = seq1.flatMap(_.generate.process)
println("run to end without OOM")

这也在不抛出 OOM 错误的情况下运行,希望它更接近LazilyEvaluatedBigData的初衷。

似乎不可能用某种递归方法来替换flatMap,以确保尽快seq的处理部分被gc,因为Seq可以是任何东西,例如Vector,在不重建结构的其余部分的情况下,要从头部分离出来并不容易。如果您将Seq替换为List,则可能会尝试构建flatMap的替代方案,其中head可以更容易地进行 gc。


编辑

如果您可以获得List而不是Seq(以便头部可以 gc'd),那么这也有效:

class LazilyEvaluatedBigData(a: String) {
lazy val generate: BigData = new BigData(128)
}
class BigData(m: Int) {
val data = Array.ofDim[Byte](1000000 * m)
def process: Seq[Int] = List(1,2,3)
}
@annotation.tailrec
def gcFriendlyFlatMap[A](xs: List[LazilyEvaluatedBigData], revAcc: List[A], f: BigData => List[A]): List[A] = {
xs match {
case h :: t => gcFriendlyFlatMap(t, f(h.generate).reverse ::: revAcc, f)
case Nil => revAcc.reverse
}
}
val results1 = gcFriendlyFlatMap(List.fill(100)(new LazilyEvaluatedBigData("")), Nil, _.process.toList)
println("run to end without OOM")
println("results1 = " + results1)

然而,这似乎非常脆弱。上面的示例仅因为gcFriendlyFlatMap是尾递归的。即使您添加 看似无害的包装围绕它,就像

def nicerInterfaceFlatMap[A](xs: List[LazilyEvaluatedBigData])(f: BigData => List[A]): List[A] = {
gcFriendlyFlatMap(xs, Nil, f)
}

,一切都因 OOM 而中断。我认为(@tailrec的小实验证实了这一点),这是因为对xs-List 的引用保存在nicerInterfaceFlatMap的堆栈帧上,因此头部不会被垃圾回收。

所以,如果你不能在LazilyEvaluatedBigData中更改lazy val,我宁愿建议围绕它建立一个包装器,在那里你可以控制引用。

最新更新