【问题标题】:Scala Source.fromFile memory consumptionScala Source.fromFile 内存消耗
【发布时间】:2016-08-10 17:51:28
【问题描述】:

全部,

我有一个只有 ~120MB 的 CSV 文件(称之为demo.csv

以下代码导致堆从正常大小100MB 爆炸到1.7GB,尽管加载的基础数据仅为120MB

我可以在这里做得更好吗?

case class Foo(x:String, y: Array[String])
....
val src = Source.fromFile(file)
val lines = src.getLines()
val raw = lines.map(_.split(",")).toArray
src.close()

/**
  * a map from accountId to their benchmark components
  */
val result = raw.groupBy(_.(0)).map {
  case (x, y) => Foo(x,y)
}.toArray

我知道toArray 可能是这里的问题,但是我确实需要groupBy ...并且除非我将所有内容都放入内存中,否则我无法使用它。什么是替代品?

我了解堆在groupBytoArray 阶段可能会暂时膨胀。但是由于底层数据只有 120MB,我的堆怎么可能永久增加>1G? (换句话说,任何被保留的东西似乎都不是 GC-ed)

【问题讨论】:

  • 你为什么不使用 List 或 Seq?此外,让您的结果可迭代以提高性能可能是值得的。
  • 用列表替换数组似乎没有什么区别。我不能将其保留为迭代器,因为此方法的结果对象必须是 Seq[Foo] where Foo.y: Array[Array[String]] 的形式
  • 你如何检查堆? Java 不会缩小其堆大小,它只会增长直到达到 Xmx VM 设置。 GC 只改变堆中的可用内存量,而不改变堆的大小。
  • 我正在使用 Runtime.getRuntime
  • 创建一个堆转储,安装MAT然后检查实际占用的内存量和占用它的内容。

标签: java scala csv io heap-memory


【解决方案1】:

首先,我将推荐使用专用的 CSV 解析库 - 手动解析 CSV 比看起来要复杂得多,存在很多边缘情况(比如说,如果您的某个值包含换行符怎么办? )。我们会选择kantan.csv,因为我是作者,但那里有很多高质量的库。

我们要做的是:

  • Iterator[(String, String)] 身份打开文件。
  • 折叠该迭代器,构建Map[String, List[String]],其中键是帐户 ID 和值基准数据。
  • 如果您真的热衷于 Foo 案例类,请将地图转换为该类的列表。

事不宜迟:

import kantan.csv._     // kantan.csv core types.
import kantan.csv.ops._ // syntax.

case class Foo(id: String, data: List[String])

// Open the CSV file for reading, assuming ; as column separator
// and no header row.
input.asUnsafeCsvReader[(String, String)](';', false)

// Fold on the file, aggregating data in a map
  .foldLeft(Map.empty[String, List[String]]) { case (acc, (key, value)) =>
    acc + (key -> (value :: acc.getOrElse(key, List.empty)))

// Now that we have the whole data as a Map, turn that into a List[Foo].
  }.map(r => Foo(r._1, r._2))

这永远不会多次加载输入数据,一旦将其放入聚合映射中就会丢弃每一行 - 与您的实现相反,如果我数对的话,它最终会在内存中存储 4 次(一次作为行,一次作为分割线,一次作为List[Foo],一次作为Array[Foo])。

此外,当您别无选择时,字符串也是不错的选择,但如果您有更好的类型(例如整数或日期),请改用这些类型。 int 使用的内存比它的字符串表示形式少得多。

告诉我结果如何!

【讨论】:

  • 非常感谢!这个库看起来很有希望,我们将继续探索。真棒你花时间......
【解决方案2】:

当您将典型文件读入内存时,大小会自动加倍,因为这会将单字节字符表示转换为两字节 JVM 字符。然后,由于 Oracle 在 Java 7 的单点版本中所做的更改,当您将输入拆分为子字符串时,您再次增加了一倍以上(在说更改子字符串之前引用了原始字符串的支持数组,但这导致了一个问题 - - 广泛使用的 Glassfish,因此 Oracle 改变了 JVM 的行为,将子字符串字符复制到新数组;因为您仍然拥有对原始字符串的引用以及您超过的子字符串双倍内存使用)。

根据拆分字符串的长度,内存使用量可能会增加一倍以上——每个字符串占用的内存大约比字符表示中的实际字节数多 40 字节,这是由于 String 对象本身和用于字符的数组对象。

所以我猜这将使您的 1.7GB 使用量减少一半。其余的可能是由于在 raw.groupBy 语句期间创建的临时结构,尽管我预计其中大部分将在之后发布。

在检查内存使用情况之前,您是否进行了一些延迟?通常需要这样做才能进行垃圾收集。垃圾回收完成后,您应该能够合理估计实际内存使用情况,如 runtime.totalMemory() - runtime.freeMemory()。

【讨论】:

  • 有关 Java 7 更改为 String 的背景,请参阅 reddit.com/comments/1qw73v 请注意,如果不进行此更改,很容易避免共享支持 char[],只需使用 new String(string.substring(...))。跨度>
  • 感谢您的回复。手动调用System.gc() 将整个堆减少到~980MB。我将对此进行更多试验。
  • 请注意,您应该能够通过将 val src = Source.fromFile(file) val lines = src.getLines() val raw = lines.map(_.split(",")).toArray src.close() 分离到函数调用中来减少一些额外的数据。这样,线路数据和 Source 对象都应该能够在调用后进行垃圾收集。您还可以在调用中包含 raw.groupBy,只返回结果数组。
猜你喜欢
  • 2012-04-06
  • 2014-01-01
  • 2010-10-12
  • 1970-01-01
  • 2011-10-03
  • 2012-11-24
  • 2013-10-08
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多