【发布时间】:2026-02-11 23:25:01
【问题描述】:
我正在尝试解析一个 50MB 的 CSV 文件。约 2500 行,约 5500 列,一列是字符串(日期为 yyyy-mm-dd),其余的是带有很多空点的浮点数。我需要能够访问所有数据,所以想实现完整的文件,在那个大小下应该是可能的。
我尝试了以下几个选项:
(with-open [rdr (io/reader path)] (doall (csv/read-csv rdr))))
使用line-seq 并手动将字符串解析为数字的手动方式。
我在单个 slurp 上的 JVM 使用量增加了 100MB,是文件大小的 2 倍。在解析数据时,我会增加 1-2GB,具体取决于它是如何完成的。如果我多次打开文件并将其解析为同一个变量,内存使用量会不断增加,最终会出现内存错误,程序会失败。 (我知道查看任务管理器并不是查看内存泄漏的最佳方法,但事实是程序失败了,所以某处存在泄漏)
打开文件的正确方法是什么?我的最终用例是我每天都会获取一个新文件,并且我希望服务器应用程序每天都打开文件并处理数据,而不会耗尽内存并需要重新启动服务器。
编辑:为了比较,使用 Python pandas 读取该文件将消耗大约 100MB 的内存,并且随后重新读取该文件不会继续增加内存使用量。
Edit2:这是一个使用局部原子来尝试查看发生了什么的最小示例:
(defn parse-number [s] (if (= s "") nil (read-string s)))
(defn parse-line [line]
(let [result (atom [])]
(doseq [x (clojure.string/split line #",")]
(swap! result conj (parse-number x)))
@result))
(defn line-by-line-parser [file]
(let [result (atom [])]
(with-open [rdr (clojure.java.io/reader file)]
(doseq [line (line-seq rdr)]
(swap! result conj (parse-line line)))
@result)))
;in the repl:
(def x (line-by-line-parser "C:\\temp\\history.csv")) ; memory goes up 1GB
(def x (line-by-line-parser "C:\\temp\\history.csv")) ; memory goes up an extra 1GB
; etc
非常感谢!
【问题讨论】:
-
200MB 的意义是因为 JAVA 使用 UCS-2 处理文本,而在 csv 中,大多数字符占用两个字节。字符通常为 1 个字节,有时会超过一个。
-
"如果我多次打开文件并将其解析为同一个变量,内存使用量不断上升,最终出现内存错误,程序失败。" - 你能分享一些代码来演示吗这个?可能是 MCVE。
-
CSV 文件中的每个字段在解析后成为内存中单独的 Clojure/Java 字符串。 JDK 8 中的每个 Java 字符串都需要 24 个字节的字符串对象,加上 16 字节的数组对象,加上每个字符 2 个字节(它们以 UTF-16 存储在内存中,每个字符 2 个字节,即使是 ASCII)。每个字段 40 个字节可能远大于每个字符 2 个字节,具体取决于 CSV 文件有多少个字段。如果您使用 JDK 9 或更高版本,如果字段仅包含 ASCII 字符,紧凑字符串会启用内存中每个字符 1 个字节的内存优化,但它不会减少每个字符串/字段的 40 个字节。
-
@andy_fingerhut 谢谢你的解释 - 有没有办法让 Java/Clojure 不对数据进行装箱(它们都是浮点数或零)?还有为什么每次读取的文件都会增加?
-
内存增加的另一种可能:可能是两个
(def x ...)在一行中,第二个保留之前的数据,直到新数据完全解析并创建数据结构后在内存中,因此至少在瞬间,根据 JVM,旧的和新的都是内存中的非垃圾。您可以通过在读取下一个文件之前执行(def x nil)来强制旧的垃圾成为可回收垃圾。
标签: csv memory clojure garbage-collection