以下是我的代码的简化版本:
// 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]
答案 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
然后,您的seq1
和seq2
只包含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
,我宁愿建议围绕它建立一个包装器,你可以控制你的参考。