0

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

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

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

这是代码片段:

    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)
4

7 回答 7

1

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

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

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

一般来说,我会使用数据库。

PS你可以节省一半的内存,全部写入一个文件,然后在一个字符串中重新读取文件。或者在文件上使用 java.nio ByteBuffer,一个内存映射文件。

于 2013-04-16T14:07:01.693 回答
1

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

根据您的应用程序的作用:

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

但是,如果您需要将整个文件保存在内存中,那么您将处于困境和艰难的境地。


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

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

  • 如果文件是 ASCII(或另一个 8 位字符集),则StringBuilder' 的缓冲区需要两倍的内存量......因为它char不是由byte值组成。

如果您对最终字符串中的字符数有很好的估计(例如根据文件大小),您可以通过在创建StringBuilder. 但是,你也不能小看,因为如果你稍微小看...

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

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

于 2013-04-16T14:02:24.510 回答
1

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

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

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

于 2013-04-16T14:04:36.653 回答
1

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

即使用 FileWriter 而不是 StringBuilder。

于 2013-04-16T14:02:00.420 回答
1

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

于 2013-04-16T14:54:36.700 回答
0

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

于 2013-04-16T14:50:02.013 回答
0

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

于 2013-04-16T14:12:32.190 回答