【问题标题】:How to deal with reading and processing huge text files without getting OutofMemoryError如何在不出现 OutofMemoryError 的情况下处理读取和处理巨大的文本文件
【发布时间】:2013-04-08 23:22:03
【问题描述】:

我写了一些简单的代码来读取文本文件 (>1g) 并对字符串进行一些处理。

但是,我必须处理 Java 堆空间问题,因为我尝试附加字符串(使用 StringBuilder),这些字符串在某些时候会占用大量内存。我知道我可以增加我的堆空间,例如。 G。 '-Xmx1024',但我想在这里只使用很少的内存。如何更改下面的代码来管理我的操作?

我仍然是 Java 新手,也许我在代码中犯了一些对您来说似乎很明显的错误。

这里是sn-p的代码:

    private void setInputData() {

    Pattern pat = Pattern.compile("regex");
    BufferedReader br = null;
    Matcher mat = null;

    try {
        File myFile = new File("myFile");
        FileReader fr = new FileReader(myFile);

        br = new BufferedReader(fr);
        String line = null;
        String appendThisString = null;
        String processThisString = null;
        StringBuilder stringBuilder = new StringBuilder();

        while ((line = br.readLine()) != null) {

            mat = pat.matcher(line);

            if (mat.find()) {
                appendThisString = mat.group(1);
            }

            if (line.contains("|")) {
                processThisString = line.replace(" ", "").replace("|", "\t");
                stringBuilder.append(processThisString).append("\t").append(appendThisString);
                stringBuilder.append("\n");
            }
        }
//      doSomethingWithTheString(stringBuilder.toString());
    } catch (Exception ex) {
        ex.printStackTrace();
    } finally {
        try {
            if (br != null)br.close();
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }
}

这是错误信息:

线程“主”java.lang.OutOfMemoryError 中的异常:Java 堆空间 在 java.util.Arrays.copyOf(Arrays.java:2367) 在 java.lang.AbstractStringBuilder.expandCapacity(AbstractStringBuilder.java:130) 在 java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:114) 在 java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:415) 在 java.lang.StringBuilder.append(StringBuilder.java:132) 在 Test.setInputData(Test.java:47) 在 Test.go(Test.java:18) 在 Test.main(Test.java:13)

【问题讨论】:

  • 如果您可以拆分对doSomethingWithTheString() 的调用,使其在每一行中都执行此操作,那可能会有很大帮助。
  • 好吧,我按照 Joop Eggen 的建议在我的案例中使用数据库。

标签: java


【解决方案1】:

您可以进行试运行,无需附加,而是计算总字符串长度。

如果 doSomethingWithTheString 是顺序的,那么会有其他解决方案。

您可以对字符串进行标记,从而减小大小。例如,霍夫曼压缩查找已经存在的读取字符的序列,可能扩展表,然后产生表索引。 (开源 OmegaT 翻译工具在一个地方使用这种策略来处理令牌。) 所以这取决于您想要进行的处理。看一种CSV读字典好像可行。

一般我会使用数据库。

附:您可以节省一半的内存,将所有内容写入文件,然后在一个字符串中重新读取文件。或者在文件上使用一个 java.nio ByteBuffer,一个内存映射文件。

【讨论】:

  • 霍夫曼压缩,这对我来说听起来很有趣,但我不确定我最终会在哪里进行这种压缩。但是您对索引和数据库的使用是正确的-我想数据库将是这里最好的替代选择。
【解决方案2】:

在这种情况下,您不能使用 StringBuilder。它将数据保存在内存中。 我认为您应该考虑将结果保存到文件中的每一行。

即使用 FileWriter 而不是 StringBuilder。

【讨论】:

    【解决方案3】:

    doSomethingWithTheString() 方法可能需要更改,以便它也接受 InputStream。在读取原始文件内容并逐行转换时,您应该将转换后的内容逐行写入临时文件。然后该临时文件的输入流可以发送到 doSomethingWithTheString() 方法。可能该方法需要重命名为 doSomethingWithInputStream()。

    【讨论】:

      【解决方案4】:

      从您的示例中,不清楚一旦修改了巨大的字符串,您将如何处理它。但是,由于您的修改似乎没有跨越多行,我只是将修改后的数据写入一个新文件。

      为了做到这一点,在您的 while 循环之前创建并打开一个新的 FileWriter 对象,请将您的 stringBuffer 声明移动到循环的开头,并将 stringBuffer 写入新文件的末尾循环。

      另一方面,如果您确实需要合并来自不同行的数据,请考虑使用数据库。哪种取决于您的数据的性质。如果它有一个类似记录的组织,您可能会采用关系数据库,例如Apache DerbyMySQL,否则您可能会查看所谓的 No SQL 数据库,例如CassandraMongoDB

      【讨论】:

      • 好吧,我将根据跨越多行的特征将行中的信息存储在 HashMap 中...
      【解决方案5】:

      一般策略是设计您的应用程序,使其不需要在内存中保存整个文件(或其中的一部分)。

      取决于您的应用程序做什么:

      • 您可以将中间数据写入一个文件,然后一次读回一行以进行处理。
      • 您可以将读取的每一行传递给处理算法;例如通过单独调用每一行而不是全部调用doSomethingWithTheString(...)

      但是,如果您需要将整个文件保存在内存中,那么您就处于两难境地了。


      另外需要注意的是,使用类似的StringBuilder 可能需要多达文件大小6 倍的内存。是这样的。

      • StringBuilder 需要扩展其内部缓冲区时,它会创建一个两倍于当前缓冲区大小的字符数组,然后从旧缓冲区复制到新缓冲区。此时,您分配的缓冲区空间是缓冲区扩展开始前的 3 倍。现在假设只有一个字符要附加到缓冲区。

      • 1234563 /p>

      如果您对最终字符串中的字符数有很好的估计(例如根据文件大小),则可以通过在创建 StringBuilder 时给出容量提示来避免 x3 乘数。但是,你也不能小看,因为如果你稍微小看的话......

      您还可以使用面向字节的缓冲区(例如 ByteArrayOutputStream)而不是 StringBuilder ...然后使用 ByteArrayInputStream / StreamReader / BufferedReader 管道读取它。

      但最终,在内存中保存一个大文件并不会随着文件大小的增加而扩展。

      【讨论】:

      • “您可以将中间数据写入文件,然后一次再读回一行来处理它”。我可能没有其他选择,因为我需要跨行查找功能并将此信息存储在 HashMap 中。我认为使用 StringBuilder 而不是用“+”连接字符串是个好主意。
      • @myX - 这是一个更好的主意,但它不是解决方案。显然,您必须想办法在(相对)有限的堆中管理该信息。如果不了解您的应用程序上下文,任何人都很难给您具体的建议。关键是了解doSomethingWithTheString 做了什么,以及它是否真的需要用整个字符串调用一次。 '因为如果是这样,你不可避免地会受到堆大小的限制。
      【解决方案6】:

      您确定文件中有行终止符吗?如果没有,您的 while 循环将继续循环并导致您的错误。如果是这样,可能值得尝试一次读取固定数量的字节,这样读取器就不会无限增长。

      【讨论】:

      • 是的,有一个行终止符,我之前检查过(- 你永远不知道:))。无论如何谢谢。
      【解决方案7】:

      我建议使用 Guavas FileBackedOutputStream。您获得了拥有一个会消耗磁盘 io 而不是主内存的 OutputStream 的优势。当然访问会因为磁盘 io 而变慢,但是,如果您正在处理如此大的流,并且您无法将其分块成更易于管理的大小,那么这是一个不错的选择。

      【讨论】:

      • 谢谢,我去看看类。
      猜你喜欢
      • 1970-01-01
      • 2014-09-30
      • 2012-02-07
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-02-01
      相关资源
      最近更新 更多