调试Java内存溢出错误

4
我是一名相对较新的程序员,在Java中经常遇到的问题是内存不足错误。我不想使用-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)

1
从您提供的文件大小来看,我认为增加内存是非常必要的。 - MozenRath
是的,但我已经在使用-Xmx1501m。 - Andrew
2
你认为这样做足够了吗? - MozenRath
1.) 小技巧:在使用outputstreams、inputstreams等时,要用try/catch/finally包围,并在finally块中关闭它们。 2.) 你必须使用性能分析来了解它的增长情况和真正使用资源的人,这就是真相! - Alfabravo
只是为了确保,主while循环不会结束吗?如果仍在写入的文件在上一个完成的文件被解析时还没有完成,我们将再次解析它?虽然与问题无关,但可以节省一些CPU。 - Sundeep
还有一个疑问,为什么要将数据存储在ArrayList中,然后逐个遍历呢?在读取时解析它,对于jsoup和非jsoup的写入都使用索引。当scanner.hasnext()不为真时,结束循环并写入最后一个分隔符。这是明显的优化。 - Sundeep
5个回答

3
我没有花太多时间分析你的代码(没有什么特别突出的地方),但一个很好的通用起点是熟悉免费的VisualVM工具。这个是一个合理的使用指南,尽管还有许多其他文章。
在我看来,有更好的商业分析器 - JProfiler就是其中之一 - 但至少它将显示哪些对象/类被分配了大部分内存,可能导致发生这种情况的方法堆栈跟踪。更简单地说,它显示了随时间推移的堆分配情况,您可以使用此来判断您是否未清除某些内容或者是否是不可避免的峰值。
我建议这样做而不是查看您代码的细节,因为这是一个有用的诊断技能。

谢谢,我会看一下。我同意我需要能够使用它。 - Andrew

2
更新:此问题已在JSoup 1.6.2中修复。
我认为这可能是您正在使用的JSoup解析器中的一个错误...目前JSoup.parse()文档上有一个警告“BETA:如果您遇到异常或错误的解析树,请提交错误报告。”这表明他们并不确定它在生产代码中完全安全。
我还发现了几个bug报告,其中一个提到由于JSoup静态地保留解析错误对象而导致内存不足异常,并且从JSoup 1.6.1降级到1.5.2可能是一种解决方法。

我会尝试降级,谢谢。我现在确定这是jsoup的一个bug。 - Andrew
很抱歉我无法解释原因,但我很高兴地宣布,通过降级到JSoup 1.5.2版本,我的问题已得到解决。 - Andrew

1

有点难以确定发生了什么,但我想到了两件事。

1)在某些奇怪的情况下(取决于输入文件),以下循环可能会将整个文件加载到内存中:

while(scanner2.hasNext()) {
    row.add(scanner2.next());
}

2)从堆栈跟踪看,似乎jsoupParse是问题所在。我认为这行代码Jsoup.parse(s).text();首先将s 加载到内存中,而根据字符串大小(这又取决于特定文件输入),这可能会导致OutOfMemoryError

也许以上两点的组合是问题所在。再次强调,仅通过查看代码很难确定..

这是否总是发生在相同的文件上?您是否检查过输入内容和其中的自定义分隔符?


我可以确认这绝对是一个jsoup问题,并且它总是发生在同一个文件上。明天我会尝试找出它在解析方面遇到了什么问题。有趣的是,它似乎不仅仅是字符串长度的问题,因为有其他更长的字符串它可以正常解析。 - Andrew

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"));
    }
}

1

我在想你的解析是否失败是因为你有糟糕的HTML(例如未关闭的标签、不匹配的引号或其他问题)被解析了?你可以使用输出/println来查看你在文档中到达了多远,如果有的话。Java库可能无法在耗尽内存之前理解文档/文件的结尾。

parse public static Document parse(String html) 将HTML解析为文档。由于没有指定基本URI,所以绝对URL检测依赖于HTML包含一个标记。

http://jsoup.org/apidocs/org/jsoup/Jsoup.html#parse(java.lang.String)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接