Scala大型列表分配导致大量垃圾回收

时间:2018-01-24 05:31:05

标签: scala list garbage-collection

编辑,摘要:所以,在后面的长链中,我认为"最终答案"有点难找。然而,实际上,Yuval指出大量内存的增量分配会强制堆调整大小(实际上,两个是图的外观)。普通JVM上的堆调整大小涉及完整的GC,这是最昂贵,最耗时的集合。所以,实际情况是我的进程本身并没有收集大量垃圾,而是它正在进行堆调整大小,它本身会触发昂贵的GC作为堆重组过程的一部分。我们这些比Java更熟悉Java的人更有可能分配一个简单的ArrayList,即使它导致堆大小调整,也只是少数几个对象(如果它是一个大数组,可能直接分配给old-gen)这项工作要少得多 - 因为它的对象要少得多! - 无论如何都需要完整的GC。道德很可能是其他一些结构更适合非常大的"列表"。

我试图尝试一些Scala的数据结构(实际上,使用并行的东西,但这与我碰到的问题无关)。我试图创建一个相当长的列表(打算纯粹按顺序处理它)。但是尽可能地尝试,我没有在不调用大量垃圾收集的情况下创建一个简单的列表。我相当确定我只是将新项目预先挂起到现有尾部,但GC负载表明我没有。到目前为止,我已经尝试了几种技术(我开始怀疑我误解了这个结构真正基本的东西:()

这是第一次努力:

val myList = {
  @tailrec
  def addToList(remaining:Long, acc:List[Double]): List[Double] =
    if (remaining > 0) addToList(remaining - 1, 0 :: acc)
    else acc

  addToList(10000000, Nil)
}

当我开始怀疑我知道如何做递归时,我想出了这个变异的野兽。

val myList = {
  var rv: List[Double] = Nil
  var count = 10000000
  while (count > 0) {
    rv = 0.0 :: rv
  }
  rv
}

它们都具有相同的效果:8个内核运行平稳(根据jvisualvm)和内存分配达到峰值仅超过1GB,我认为这是数据所需的实际分配空间,但在途中,它在途中会产生看似庞大的垃圾。

我在这里做了一些可怕的错事吗?我是以某种方式强迫每个新元素重新创建整个列表(我非常努力地只做#34; prepend"类型操作,我认为应该避免这种情况。)

或许,我有半个记忆,听到Scala List做了一些奇怪的事情来帮助它变成一个可变列表,或者一个并行列表,或者什么。真的不记得是什么。这与此有关吗?如果是这样的话,那是什么"那"呢?

哦,这是GC流程的形象。请注意前端加载内存的三角形上升,表示"真实"分配数据。这个巨大的驼峰和相关的CPU使用率是我的问题:Screenshot from jvisualvm showing GC activity

编辑:我应该澄清,我对两件事感兴趣。首先,如果我创建列表本质上是错误的(即如果我实际上并不只是执行前置操作)那么我想了解为什么,以及我应该如何做到这一点"对&# 34 ;.其次,如果我的构造是健全的并且奇怪的行为在列表中是固有的,我想更好地理解列表,所以我知道它在做什么,以及为什么。我并不特别感兴趣(在这一点上)以其他方式构建一个可以回避这个问题的顺序数据结构。我期待很多使用List,并想知道发生了什么。 (后来,我可能想要在这个细节级别调查其他结构,但现在不行。)

1 个答案:

答案 0 :(得分:1)

  

首先,如果我创建列表本质上是错误的(即如果   我实际上并不只是执行前置操作)然后我想   理解为什么

您正在正确构建列表,那里没有问题。

  

其次,如果我的构造是健全的并且奇怪的行为是固有的   在列表中,我想更好地理解列表,所以我知道什么   它正在做,为什么

Scala中的

List[A]基于链表实现,您的头部类型为A,尾部类型为List[A]List[A]是一个抽象类,有两个实现,一个显示名为Nil的空列表,一个名为“Cons”,或::,表示具有头值和尾部的列表,可以是满的也可以是空的:

def ::[B >: A] (x: B): List[B] =
  new scala.collection.immutable.::(x, this)

如果我们查看::的实现,我们可以看到它是一个包含两个字段的简单案例类:

final case class ::[B](override val head: B, private[scala] var tl: List[B]) extends List[B] {
  override def tail : List[B] = tl
  override def isEmpty: Boolean = false
}

使用IntelliJ中的内存选项卡快速查看显示:

Memory overhead of creating a List

我们有一千万个Double值和一千万个::案例类的实例,这本身就有一个案例类的额外开销(编译器“增强”这些类的附加功能结构)。

您的JVisualVM实例未显示GC图表被充分利用,而是显示您的CPU因生成大量项目而过度工作。在分配过程中,您会生成大量中间列表,直到达到完全生成的列表,这意味着必须在不同的GC级别之间驱逐数据(Eden,Survivor和Old,假设您正在运行Scala的JVM风格)

如果我们想要更多信息,我们可以使用Mission Control来查看导致内存压力的因素。这是从30秒运行的配置文件生成的示例:

def main(args: Array[String]): Unit = {
  def myList: List[Double] = {
    @tailrec
    def addToList(remaining:Long, acc:List[Double]): List[Double] =
      if (remaining > 0) addToList(remaining - 1, 0 :: acc)
      else acc

    addToList(10000000, Nil)
  }

  while (true) {
    myList
  }
}

Mission Control Profile

我们看到我们调用了BoxesRunTime.boxToDouble,因为::是一个通用类而且@specialized没有double属性。我们去scala.Int -> scala.Double -> java.lang.Double