Scala 可迭代内存泄漏

Posted

技术标签:

【中文标题】Scala 可迭代内存泄漏【英文标题】:Scala Iterable Memory Leaks 【发布时间】:2012-09-12 08:44:45 【问题描述】:

我最近开始使用 Scala 并遇到以下问题。下面是 4 种不同的方法来遍历文件的行,做一些事情,并将结果写入另一个文件。其中一些方法如我所想(尽管使用大量内存来做到这一点),而另一些则无休止地消耗内存。

这个想法是将 Scala 的 getLines Iterator 包装为一个 Iterable。我不在乎它是否会多次读取文件——这就是我期望它做的事情。

这是我的复制代码:

class FileIterable(file: java.io.File) extends Iterable[String] 
  override def iterator = io.Source.fromFile(file).getLines


// Iterator

// Option 1: Direct iterator - holds at 100MB
def lines = io.Source.fromFile(file).getLines

// Option 2: Get iterator via method - holds at 100MB
def lines = new FileIterable(file).iterator

// Iterable

// Option 3: TraversableOnce wrapper - holds at 2GB
def lines = io.Source.fromFile(file).getLines.toIterable

// Option 4: Iterable wrapper - leaks like a sieve
def lines = new FileIterable(file)

def values = lines
      .drop(1)
      //.map(l => l.split("\t")).map(l => l.reduceLeft(_ + "|" + _))
      //.filter(l => l.startsWith("*"))

val writer = new java.io.PrintWriter(new File("out.tsv"))
values.foreach(v => writer.println(v))
writer.close()

它正在读取的文件约为 10GB,其中 1MB 行。

前两个选项使用恒定的内存量 (~100MB) 迭代文件。这是我所期望的。这里的缺点是迭代器只能使用一次,并且它使用 Scala 的按名称调用约定作为伪可迭代。 (作为参考,等效的 c# 代码使用 ~14MB)

第三个方法调用定义在 TraverableOnce 中的 toIterable。这个工作,但它使用大约 2GB 来做同样的工作。不知道内存的去向,因为它无法缓存整个 Iterable。

第四个是最令人震惊的——它立即使用所有可用内存并抛出 OOM 异常。更奇怪的是,它对我测试过的所有操作都执行此操作:drop、map 和 filter。查看实现,它们似乎都没有保持太多状态(尽管下降看起来有点可疑 - 为什么它不只计算项目?)。如果我不做任何操作,它就可以正常工作。

我的猜测是它在某处维护对所读取的每一行的引用,尽管我无法想象如何。在 Scala 中传递 Iterables 时,我看到了相同的内存使用情况。例如,如果我采用案例 3 (.toIterable) 并将其传递给将 Iterable[String] 写入文件的方法,我会看到同样的爆炸。

有什么想法吗?

【问题讨论】:

【参考方案1】:

注意ScalaDoc of Iterable 的说法:

这个 trait 的实现需要提供一个具体的方法 签名:

  def iterator: Iterator[A]

他们还需要提供一个方法 newBuilder 来创建一个构建器 用于同类收藏。

由于您没有为 newBuilder 提供实现,因此您将获得默认实现,它使用 ListBuffer 并因此尝试将所有内容放入内存中。

您可能希望将Iterable.drop 实现为

def drop(n: Int) = iterator.drop(n).toIterable

但这会破坏集合库的表示不变性(即iterator.toIterable 返回Stream,而您希望List.drop 返回List 等 - 因此需要Builder 概念) .

【讨论】:

有趣...我来自 c#,所有这些都得到了照顾。出于好奇——他们为什么会选择缓冲整个序列作为默认选项? 这是否也意味着当我将序列作为 Iterable[T] 参数传递时,默认情况下它会被缓冲?如果是这样,那不是违背了目的吗?我的印象是,当我通过 toList、toArray 等明确要求数据时,数据只会被缓冲在内存中。 收藏库的设计我真的没有资格评论(主题的标准介绍是here)。您实际上只是遇到问题,因为您正在尝试扩展 Iterable,您可以使用 Stream 或 Iterator。

以上是关于Scala 可迭代内存泄漏的主要内容,如果未能解决你的问题,请参考以下文章

无法隔离 Scala 批量数据加载应用程序中的 JDBC 内存泄漏

迭代器会导致内存泄漏吗?

DateFormatter 中的内存泄漏

关于右值的范围和内存泄漏

UIGraphicsGetImageFromCurrentImageContext 内存泄漏与预览

TensorRT 增加内存使用(泄漏?)