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

时间:2018-03-07 07:21:55

标签: scala memory-management garbage-collection scala-collections flatmap

以下是我的代码的简化版本:

// 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]

1 个答案:

答案 0 :(得分:5)

您对GC没有太多期待,但是您假设代码没有表达的内容。

你有一个

lazy val generate: BigData

在你的LazilyEvaluatedBigData课程中,你有

val seq1: Seq[LazilyEvaluatedBigData]
正在执行的代码中的

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

  • lazy val不是def:一旦被调用,它就会保证它会存储评估结果。如果你的程序内存不足,你不应该期望它会让它的值被垃圾收集,并且一旦需要它就重新计算它。
  • Seq保证它不会丢失任何元素。例如,List永远不会因为程序内存不足而丢弃任何元素。你可能需要像软件引用这样的序列,或者你必须重写你的代码,以便在不再需要时不会引用带有处理元素的列表头部。

如果你综合考虑这两点,那么你的代码基本上会说flatMap seq1的末尾是一个包含多个LazilyEvaluatedBigData实例引用的序列,这些lazy val - 实例中的LazilyEvaluatedBigData都被评估并保存在内存中。

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

def generate: BigData

然后,您的seq1seq2只包含String s的精简包装,而flatMap的每一步都会加载一个BigData实例,壁球它再次使用Seq[Int]变成一个小process,然后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'd,因为Seq可以是任何东西,例如一个Vector,在没有重建结构的其余部分的情况下拆分头部并不容易。如果您将flatMap替换为Seq,可以尝试构建List的替代方案,其中head可以更容易gc。

修改

如果代替Seq,您可以获得List(以便头部可以被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的堆栈帧上,因此头部不能是垃圾集。

所以,如果你不能改变lazy val中的LazilyEvaluatedBigData,我宁愿建议围绕它建立一个包装器,你可以控制你的参考。