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-titlerevision-userrevision-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 堆空间

将 Clojure 命名空间拆分为多个文件

java.lang.OutOfMemoryError:DBeaver 中的 Java 堆空间 [重复]

Clojure 中的命名空间之间共享函数

OutOfMemoryError:Java 堆空间。如何修复递归方法中发生的这个错误?