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

Should GC occur on processed elements of 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 class 实例。鉴于不需要将 seq1 或 seq2 的 'processed' 元素保存在内存中,我希望它们被垃圾收集 - 但是我的进程在 flatMaps 的中间保持 OOMing :(

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


最终修复是合并此 class:

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

然后将seq1和seq2转换为:

val seq1: Seq[OnDemandLazilyEvaluatedBigData]

您对 GC 的期望不高,但您假设了您的代码未表达的内容。

你有一个

lazy val generate: BigData

在您的 LazilyEvaluatedBigData class 中,您有

val seq1: Seq[LazilyEvaluatedBigData]

在正在执行的代码中。

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

  • A lazy val 不是 def:一旦被调用,它保证将存储计算结果。如果您的程序 运行 内存不足,您不应该期望它会让它的值为 garbage-collected,并在再次需要时重新计算它。
  • A Seq 保证它不会丢失任何元素。例如,List 永远不会仅仅因为程序 运行 内存不足而丢弃它的任何元素。为此,您需要类似带有软引用的序列,或者您必须重写代码,以便在不再需要时不再引用包含已处理元素的列表头部。

如果您将这两点结合起来考虑,那么您的代码实质上是说 flatMap 的末尾 seq1 是一个包含多个 LazilyEvaluatedBigData- 引用的序列实例,以及那些 LazilyEvaluatedBigData 实例中的 lazy vals 都被评估并保存在内存中。


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

def generate: BigData

那么你的 seq1seq2 将只包含 String 的薄包装,并且 flatMap 的每一步都会加载一个 BigData 实例,使用 process 再次将其压缩成一个微小的 Seq[Int],然后 BigData 实例可以再次成为 garbage-collected。这在没有太多内存的情况下成功运行:

// 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-errors的情况下运行的,希望能更接近LazilyEvaluatedBigData的初衷。

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


编辑

如果你可以得到一个 List 而不是 Seq(这样 heads 就可以被 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,我宁愿建议围绕它构建一个包装器,您可以在其中控制引用。