[英]How to deal with reading and processing huge text files without getting OutofMemoryError
我编写了一些简单的代码来读取文本文件(> 1g)并对String进行一些处理。
但是,我必须处理Java堆空间问题,因为我尝试附加String(使用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();
}
}
}
这是错误消息:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at java.util.Arrays.copyOf(Arrays.java:2367) at java.lang.AbstractStringBuilder.expandCapacity(AbstractStringBuilder.java:130) at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:114) at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:415) at java.lang.StringBuilder.append(StringBuilder.java:132) at Test.setInputData(Test.java:47) at Test.go(Test.java:18) at Test.main(Test.java:13)
在这种情况下,您不能使用StringBuilder。 它在内存中保存数据。 我认为您应该考虑将结果保存到文件的每一行中。
即使用FileWriter而不是StringBuilder。
一般的策略是设计您的应用程序,以便它不需要将整个文件(或文件的太大比例)保存在内存中。
根据您的应用程序执行以下操作:
doSomethingWithTheString(...)
。 但是,如果您需要将整个文件存储在内存中,那么您将陷入困境。
要注意的另一件事是,使用类似的StringBuilder
可能需要多达文件大小6倍的内存。 就像这样
当StringBuilder
需要扩展其内部缓冲区时,它可以通过使char数组的大小是当前缓冲区的两倍,并从旧版本复制到新版本来完成此操作。 那时,您分配的缓冲区空间是缓冲区扩展开始之前的3倍。 现在,假设仅一个字符要追加到缓冲区。
如果文件是ASCII(或其他8位字符集)格式,则StringBuilder
的缓冲区需要两倍的内存...,因为它是由char
而不是byte
值组成的。
如果您对最终字符串中的字符数有很好的估计(例如,从文件大小来看),则可以在创建StringBuilder
时通过提供容量提示来避免x3乘数。 但是,您一定不能低估,因为您可能会略微低估...
您也可以使用面向字节的缓冲区(例如ByteArrayOutputStream
)代替StringBuilder ...,然后使用ByteArrayInputStream
/ StreamReader
/ BufferedReader
管道读取它。
但是最终,随着文件大小的增加,在内存中保存大文件不会扩展。
从您的示例中,尚不清楚一旦修改了巨大的字符串,您将如何处理它。 但是,由于您的修改似乎没有跨越多行,因此我只将修改后的数据写入新文件。
为了做到这一点,在while
循环之前创建并打开一个新的FileWriter
对象,请将stringBuffer
声明移至循环的开始,并在循环结束时将stringBuffer
写入新文件。
另一方面,如果您确实需要合并来自不同行的数据,请考虑使用数据库。 哪种类型取决于数据的性质。 如果它具有类似记录的组织,则可以采用关系数据库,例如Apache Derby或MySQL ,否则您可以签出所谓的No SQL数据库,例如Cassandra或MongoDB 。
您可以进行空运行,而无需附加,但要计算字符串的总长度。
如果doSomethingWithTheString是顺序的,将有其他解决方案。
您可以标记字符串,以减小大小。 例如,霍夫曼压缩查找已存在的读取char的序列,可能扩展表,然后产生表索引。 (开放源代码的OmegaT转换工具在一处针对令牌使用这种策略。)因此,这取决于您要执行的处理。 看到一种CSV的阅读字典似乎是可行的。
通常,我会使用数据库。
PS,您可以节省一半的内存,全部写入一个文件,然后以一个字符串重新读取该文件。 或在文件(使用内存映射的文件)上使用java.nio ByteBuffer。
doSomethingWithTheString()方法可能需要更改,以便它也接受InputStream。 在读取原始文件内容并逐行转换时,应将转换后的内容逐行写入临时文件。 然后,可以将该临时文件的输入流发送到doSomethingWithTheString()方法。 可能需要将该方法重命名为doSomethingWithInputStream()。
您确定文件中有行终止符吗? 如果没有,您的while循环将一直循环并导致错误。 如果是这样,可能值得尝试一次读取固定数量的字节,以使读取器不会无限增长。
我建议使用番石榴FileBackedOutputStream。 您将获得拥有OutputStream的优势,它将占用磁盘io而不是主内存。 当然,由于磁盘io,访问速度会变慢,但是,如果要处理如此大的流,并且无法将其分块为更大的可管理大小,则这是一个不错的选择。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.