Scala 可迭代对象内存泄漏问题

6
我最近开始尝试使用Scala,并遇到了以下问题。以下是4种不同的迭代文件行的方法,执行一些操作并将结果写入另一个文件。其中一些方法按照我的想法工作(尽管使用了大量内存),而有些则会无限制地消耗内存。
这个想法是将Scala的getLines迭代器包装为可迭代对象。我不在乎它是否多次读取文件 - 这就是我期望它做的事情。
下面是我的复现代码:
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。查看实现,它们似乎都没有保留太多状态(尽管drop看起来有点可疑 - 为什么它不只计算项目数?)。如果我不执行任何操作,它就可以正常工作。
我的猜测是某个地方它在维护对已读取的每行的引用,虽然我无法想象是如何做到的。在Scala中传递可迭代对象时,我看到了相同的内存使用情况。例如,如果我采用第3种方法(.toIterable)并将其传递给一个将Iterable[String]写入文件的方法,我会看到相同的内存爆炸。
有什么想法吗?
1个回答

6
请注意,可迭代对象的ScalaDoc中写道:

Implementations of this trait need to provide a concrete method with signature:

  def iterator: Iterator[A]

They also need to provide a method newBuilder which creates a builder for collections of the same kind.

由于您没有提供newBuilder的实现,因此您将获得默认实现,它使用ListBuffer,因此尝试将所有内容都适合内存。您可能希望实现Iterable.drop如下:
def drop(n: Int) = iterator.drop(n).toIterable

但这将违反集合库的表示不变性(即iterator.toIterable返回一个Stream,而您希望List.drop返回一个List等),因此需要使用Builder概念。


1
有趣...我来自c#,那里所有的事情都被照顾了。出于好奇 - 为什么他们会选择将整个序列作为默认选项进行缓冲? - Matt Bossenbroek
这是否意味着,当我将序列作为 Iterable[T] 参数传递时,默认情况下它会被缓冲?如果是这样,那不是违背了初衷吗?我原本以为只有在通过 toList、toArray 等显式要求时才会在内存中缓冲数据。 - Matt Bossenbroek
我并不真正有资格对集合库的设计发表评论(该主题的标准介绍在此处:http://www.artima.com/scalazine/articles/scala_collections_architecture.html)。你遇到问题只是因为你试图扩展Iterable,如果使用Stream或Iterator,就不会有问题了。 - themel

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接