我仍然是一个相对较新的程序员,我在Java中遇到的一个问题是Out of Memory Errors。我不想使用-Xmx来增加内存,因为我觉得错误是由于编程不好造成的,我想改进编码而不是依靠更多的内存。
我所做的工作涉及处理大量文本文件,每个文本文件在压缩时大约为1GB。我在这里的代码是为了遍历一个新的压缩文本文件被删除的目录。它打开了第二个最新的文本文件(不是最新的,因为它仍然被写入),并使用Jsoup库来解析文本文件中的某些字段(字段用自定义分隔符分隔:“| nTa |”表示一个新列,“| nLa |”表示一个新行。
我觉得应该没有理由使用大量内存。我打开一个文件,扫描它,解析相关位,将解析后的版本写入另一个文件,关闭文件,然后移动到下一个文件。我不需要将整个文件存储在内存中,我当然不需要存储已在内存中处理过的文件。
当我开始解析第二个文件时,我遇到错误,这表明我没有处理垃圾收集。请查看代码,看看你是否能发现我正在做的事情,这意味着我使用的内存比我应该的多。我想学习如何做到这一点,所以我不会出现内存错误!
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Scanner;
import java.util.TreeMap;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
import org.jsoup.Jsoup;
public class ParseHTML {
public static int commentExtractField = 3;
public static int contentExtractField = 4;
public static int descriptionField = 5;
public static void main(String[] args) throws Exception {
File directoryCompleted = null;
File filesCompleted[] = null;
while(true) {
// find second most recent file in completed directory
directoryCompleted = new File(args[0]);
filesCompleted = directoryCompleted.listFiles();
if (filesCompleted.length > 1) {
TreeMap<Long, File> timeStamps = new TreeMap<Long, File>(Collections.reverseOrder());
for (File f : filesCompleted) {
timeStamps.put(getTimestamp(f), f);
}
File fileToProcess = null;
int counter = 0;
for (Long l : timeStamps.keySet()) {
fileToProcess = timeStamps.get(l);
if (counter == 1) {
break;
}
counter++;
}
// start processing file
GZIPInputStream gzipInputStream = null;
if (fileToProcess != null) {
gzipInputStream = new GZIPInputStream(new FileInputStream(fileToProcess));
}
else {
System.err.println("No file to process!");
System.exit(1);
}
Scanner scanner = new Scanner(gzipInputStream);
scanner.useDelimiter("\\|nLa\\|");
GZIPOutputStream output = new GZIPOutputStream(new FileOutputStream("parsed/" + fileToProcess.getName()));
while (scanner.hasNext()) {
Scanner scanner2 = new Scanner(scanner.next());
scanner2.useDelimiter("\\|nTa\\|");
ArrayList<String> row = new ArrayList<String>();
while(scanner2.hasNext()) {
row.add(scanner2.next());
}
for (int index = 0; index < row.size(); index++) {
if (index == commentExtractField ||
index == contentExtractField ||
index == descriptionField) {
output.write(jsoupParse(row.get(index)).getBytes("UTF-8"));
}
else {
output.write(row.get(index).getBytes("UTF-8"));
}
String delimiter = "";
if (index == row.size() - 1) {
delimiter = "|nLa|";
}
else {
delimiter = "|nTa|";
}
output.write(delimiter.getBytes("UTF-8"));
}
}
output.finish();
output.close();
scanner.close();
gzipInputStream.close();
}
}
}
public static Long getTimestamp(File f) {
String name = f.getName();
String removeExt = name.substring(0, name.length() - 3);
String timestamp = removeExt.substring(7, removeExt.length());
return Long.parseLong(timestamp);
}
public static String jsoupParse(String s) {
if (s.length() == 4) {
return s;
}
else {
return Jsoup.parse(s).text();
}
}
}
如何确保在完成对象后,它们被销毁并且不使用任何资源?例如,每次我关闭GZIPInputStream,GZIPOutputStream和Scanner时,我怎样才能确保它们被彻底销毁?
为了记录,我得到的错误是:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:2882)
at java.lang.AbstractStringBuilder.expandCapacity(AbstractStringBuilder.java:100)
at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:572)
at java.lang.StringBuilder.append(StringBuilder.java:203)
at org.jsoup.parser.TokeniserState$47.read(TokeniserState.java:1171)
at org.jsoup.parser.Tokeniser.read(Tokeniser.java:42)
at org.jsoup.parser.TreeBuilder.runParser(TreeBuilder.java:101)
at org.jsoup.parser.TreeBuilder.parse(TreeBuilder.java:53)
at org.jsoup.parser.Parser.parse(Parser.java:24)
at org.jsoup.Jsoup.parse(Jsoup.java:44)
at ParseHTML.jsoupParse(ParseHTML.java:125)
at ParseHTML.main(ParseHTML.java:81)
答案 0 :(得分:3)
我没有花很长时间分析你的代码(没有什么突出的),但一个好的通用开始就是熟悉免费的VisualVM工具。 This是使用它的合理指南,尽管还有更多文章。
在我看来有更好的商业分析器 - 一个JProfiler - 但它至少会告诉你大多数内存被分配给哪些对象/类,以及可能导致这种情况发生的方法堆栈跟踪。更简单地说,它会显示您随时间的堆分配,并且您可以使用它来判断您是否未能清除某些内容或是否是不可避免的峰值。
我建议这样做,而不是查看代码的细节,因为它是一种有用的诊断技能。
答案 1 :(得分:2)
更新:此问题已在JSoup 1.6.2
中修复在我看来,它可能是你正在使用的JSoup解析器中的一个错误...目前documentation for JSoup.parse()有一个警告“BETA:如果你确实引发了异常,或者是一个糟糕的解析-tree,请提交一个错误。“这表明他们不相信在生产代码中使用它是完全安全的。
我还发现了一些错误报告,提到了内存异常,one of which表明它是由于JSoup静态保存解析错误对象,并且从JSoup 1.6.1降级到1.5.2可能是一项工作-around。
答案 2 :(得分:1)
我想知道你的解析是否失败,因为你有不好的HTML(例如未关闭的标签,不成对的引号或诸如此类)被解析?您可以执行输出/ println以查看您在文档中获得的距离(如果有的话)。在内存不足之前,Java库可能无法理解文档/文件的结尾。
解析 public static Document parse(String html)将HTML解析为Document。由于没有指定基URI,绝对URL检测依赖于包含标记的HTML。
http://jsoup.org/apidocs/org/jsoup/Jsoup.html#parse(java.lang.String)
答案 3 :(得分:1)
有点难以分辨出发生了什么,但我想到了两件事。
1)在一些奇怪的情况下(取决于输入文件),以下循环可能会将整个文件加载到内存中:
while(scanner2.hasNext()) {
row.add(scanner2.next());
}
2)通过查看stackTrace,似乎jsoupParse是问题所在。我相信这行Jsoup.parse(s).text();
首先将s
加载到内存中,并且取决于字符串大小(这又取决于特定的文件输入),这可能会导致OutOfMemoryError
可能上述两点的组合就是问题。再一次,通过查看代码很难说出来。
这是否始终使用相同的文件?您是否检查了输入内容和自定义分隔符?
答案 4 :(得分:1)
假设JSoup代码中没有问题,我们可以做一些内存优化。在示例中,ArrayList<String> row
可以被剥离,因为它在内存中保存所有已解析的行,但只需要一行解析。
删除了row
的内部循环:
//Caution! May contain obvious bugs!
while (scanner.hasNext()) {
String scanStr = scanner.next();
//manually count of rows to replace 'row.size()'
int rowCount = 0;
int offset = 0;
while ((offset = scanStr.indexOf("|nTa|", offset)) >= 0) {
rowCount++;
offset++;
}
rowCount++;
Scanner scanner2 = new Scanner(scanStr);
scanner2.useDelimiter("\\|nTa\\|");
int index = 0;
while (scanner2.hasNext()) {
String curRow = scanner2.next();
if (index == commentExtractField
|| index == contentExtractField
|| index == descriptionField) {
output.write(jsoupParse(curRow).getBytes("UTF-8"));
} else {
output.write(curRow.getBytes("UTF-8"));
}
String delimiter = "";
if (index == rowCount - 1) {
delimiter = "|nLa|";
} else {
delimiter = "|nTa|";
}
output.write(delimiter.getBytes("UTF-8"));
}
}