Clojure 中的巨大文件和 Java 堆空间错误
Posted
技术标签:
【中文标题】Clojure 中的巨大文件和 Java 堆空间错误【英文标题】:Huge file in Clojure and Java heap space error 【发布时间】:2012-04-16 13:25:21 【问题描述】:我之前在huge XML file 上发布过——它是一个 287GB 的 XML,带有我想放入 CSV 文件(修订作者和时间戳)的***转储。我设法做到了这一点。在我得到 *** 错误之前,但现在在解决第一个问题之后我得到:java.lang.OutOfMemoryError: Java heap space error。
我的代码(部分取自 Justin Kramer 的回答)如下所示:
(defn process-pages
[page]
(let [title (article-title page)
revisions (filter #(= :revision (:tag %)) (:content page))]
(for [revision revisions]
(let [user (revision-user revision)
time (revision-timestamp revision)]
(spit "files/data.csv"
(str "\"" time "\";\"" user "\";\"" title "\"\n" )
:append true)))))
(defn open-file
[file-name]
(let [rdr (BufferedReader. (FileReader. file-name))]
(->> (:content (data.xml/parse rdr :coalescing false))
(filter #(= :page (:tag %)))
(map process-pages))))
我不显示article-title
、revision-user
和revision-title
函数,因为它们只是从页面或修订哈希中的特定位置获取数据。任何人都可以帮我解决这个问题——我是 Clojure 的新手,没有遇到问题。
【问题讨论】:
【参考方案1】:要明确一点,(:content (data.xml/parse rdr :coalescing false))
是懒惰的。如果您不相信,请检查其类或拉出第一项(它会立即返回)。
也就是说,在处理大型序列时需要注意几件事:抓住头部,以及未实现/嵌套的惰性。我认为您的代码受到后者的影响。
以下是我的建议:
1) 将(dorun)
添加到->>
调用链的末尾。这将迫使序列完全实现,而不用抓住头。
2) 将 process-page
中的 for
更改为 doseq
。您正在向文件吐口水,这是一个副作用,您不想在这里懒惰地这样做。
正如 Arthur 建议的那样,您可能希望打开一个输出文件并继续写入,而不是为每个 Wikipedia 条目打开和写入(吐出)。
更新:
这是一个尝试更清楚地分离关注点的重写:
(defn filter-tag [tag xml]
(filter #(= tag (:tag %)) xml))
;; lazy
(defn revision-seq [xml]
(for [page (filter-tag :page (:content xml))
:let [title (article-title page)]
revision (filter-tag :revision (:content page))
:let [user (revision-user revision)
time (revision-timestamp revision)]]
[time user title]))
;; eager
(defn transform [in out]
(with-open [r (io/input-stream in)
w (io/writer out)]
(binding [*out* out]
(let [xml (data.xml/parse r :coalescing false)]
(doseq [[time user title] (revision-seq xml)]
(println (str "\"" time "\";\"" user "\";\"" title "\"\n")))))))
(transform "dump.xml" "data.csv")
我在这里看不到任何会导致过度使用内存的东西。
【讨论】:
对于 Clojure 新手来说,关于 dorun 的观点可能会更清楚一点:问题中显示的打开文件函数返回对进程页面的调用结果序列,以及函数何时返回从 repl 调用,打印序列会导致所有结果同时保存在内存中。对结果调用 dorun 会导致对序列中的元素进行求值并返回 nil,因此无需将所有结果同时保存在内存中。 感谢您的解释!我现在确实(希望)理解了这个代码 sn-p 中的懒惰是如何工作的,并改变了你提出的建议,但仍然是OutOfMemoryError: Java heap space
。我正在处理最终文件的 1GB 样本,但它仍然会引发内存错误。非常感谢您的帮助。
查看我的最新更新。如果您仍然收到 OutOfMemory 错误,我不知道为什么。我使用的代码与此非常相似,没有内存问题。
故障排除思路:是否总是在同一项目上耗尽内存?该项目是否与众不同(例如,真的很大,有很多修订)?您是否尝试过为 JVM 提供更多内存?您确定您没有在任何地方保留任何子字符串(JVM 不会 GC 字符串仍在使用中的子字符串)?
基本上 - 非常感谢您提供的所有帮助。 A 花了更多时间使用它,就 JVM 调整而言,这对我来说太复杂了,而且我尝试使用一些内存选项,我得到更多花哨的错误。在我能够正确处理这个问题之前,我可能会花更多时间在 Clojure 和 JVM 上。【参考方案2】:
不幸的是data.xml/parse
并不懒惰,它会尝试将整个文件读入内存然后解析它。
改为使用 this (lazy) xml library,它只保存它当前在ram 中处理的部分。然后,您需要重新构建代码以在读取输入时写入输出,而不是收集所有 xml,然后输出。
你的线
(:content (data.xml/parse rdr :coalescing false)
会将所有 xml 加载到内存中,然后从中请求内容密钥。这会炸毁堆。
懒惰答案的大致轮廓如下所示:
(with-open [input (java.io.FileInputStream. "/tmp/foo.xml")
output (java.io.FileInputStream. "/tmp/foo.csv"]
(map #(write-to-file output %)
(filter is-the-tag-i-want? (parse input))))
要有耐心,与(> data ram)
合作总是需要时间:)
【讨论】:
他已经在使用来自 contrib 的data.xml
,正如你所指出的,它是懒惰的。【参考方案3】:
我不了解 Clojure,但在纯 Java 中,可以使用基于 SAX 事件的解析器,例如 http://docs.oracle.com/javase/1.4.2/docs/api/org/xml/sax/XMLReader.html 不需要将 XML 加载到 RAM 中
【讨论】:
以上是关于Clojure 中的巨大文件和 Java 堆空间错误的主要内容,如果未能解决你的问题,请参考以下文章
在 Clojure 中需要命名空间时出现 FileNotFoundException
OutOfMemoryError : Spark 中的 Java 堆空间