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 val
s 都被评估并保存在内存中。
如果您希望 BigData
实例在 flatMap
期间不再需要时被垃圾回收,只需将 generate
声明为
def generate: BigData
那么你的 seq1
和 seq2
将只包含 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
,我宁愿建议围绕它构建一个包装器,您可以在其中控制引用。
这是我的代码的简化版本:
// 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 val
s 都被评估并保存在内存中。
如果您希望 BigData
实例在 flatMap
期间不再需要时被垃圾回收,只需将 generate
声明为
def generate: BigData
那么你的 seq1
和 seq2
将只包含 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
,我宁愿建议围绕它构建一个包装器,您可以在其中控制引用。