在Java中读取大文件 -- Java堆空间

7

我正在读取一个大的tsv文件(约40G),并尝试通过逐行读取并仅将某些行打印到新文件中来修剪它。然而,我一直收到以下异常:

java.lang.OutOfMemoryError: Java heap space
    at java.util.Arrays.copyOf(Arrays.java:2894)
    at java.lang.AbstractStringBuilder.expandCapacity(AbstractStringBuilder.java:117)
    at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:532)
    at java.lang.StringBuffer.append(StringBuffer.java:323)
    at java.io.BufferedReader.readLine(BufferedReader.java:362)
    at java.io.BufferedReader.readLine(BufferedReader.java:379)

以下是主要代码部分。我将缓冲区大小指定为8192,以防万一。Java不会在达到缓冲区大小限制后清除缓冲区吗?我不知道这里可能导致大量内存使用的原因。我尝试增加堆大小,但没有任何改变(4GB RAM的机器)。我还尝试每X行刷新输出文件,但也没有帮助。我想也许我需要调用GC,但这听起来不太对。有什么想法吗?非常感谢。 顺便说一句——我知道只应该调用trim()一次,将其存储,然后使用它。
Set<String> set = new HashSet<String>();
set.add("A-B");
...
...
static public void main(String[] args) throws Exception
{
   BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(inputFile),"UTF-8"), 8192);
   PrintStream output = new PrintStream(outputFile, "UTF-8");

   String line = reader.readLine();
   while(line!=null){
        String[] fields = line.split("\t");
        if( set.contains(fields[0].trim()+"-"+fields[1].trim()) )
            output.println((fields[0].trim()+"-"+fields[1].trim()));

        line = reader.readLine();
   }

output.close();

}

3
它是否打印任何结果?它是否总是在同一点崩溃?你确定它实际上是逐行读取文件,即它正确地识别了行尾,并且没有行太长而导致堆崩溃?这些问题可能有些愚蠢,但我还是要问一下。 - Trevor Tippins
你在JVM上使用了什么-Xmx设置?默认情况下,除非你使用-Xmx参数指定,否则Java不会使用机器上所有可用的RAM。 - bstick12
4
@bstick12:为您的应用程序提供更多的内存可能会隐藏一些重要的设计错误,这些错误将在后期出现。在99.99%的情况下,默认内存已足够使用,如果不够用,则说明您正在做错什么。 - gd1
99.99%的情况下,默认内存是足够的,如果不够用,那么你可能做错了什么。那么如何解释大多数生产服务器以及某种程度上许多Java应用程序都会增加默认值呢?难道99.99%的这些人都在做错事吗? - Nicolas Bousquet
5个回答

17

很可能的情况是文件中没有行终止符号,因此读取器会不断在其StringBuffer中增加内容,直到耗尽内存。

解决方法是每次读取固定字节数,使用读取器的“read”方法,然后在较小的缓冲区内查找新行(或其他解析标记)。


这可能是NIO包的一个好地方 - 他需要尽可能多的性能来处理大约40GB的文本数据。 - Steven Fines
现在很有道理,因为我注意到无论我设置最大堆大小为多少,最终输出文件大小总是相同的。所以我怀疑某个地方有一行代码引起了问题。我正在检查它。非常感谢! - user431336
@user431336:并且不要忘记关闭你的PrintStream... 当你终止方法时,你的例子将会让它保持开启状态。 - Steven Fines
@Dataknife,PrintStream是吗?一旦循环结束,我会关闭它。 - user431336
检查缺少行终止符的损坏文件应该是首要任务 - 当我读取4GB ASCII文件时,我遇到了完全相同的情况。尝试使用命令“tail <your file_name>”查看是否正确打印和存在。对于损坏的文件,它将无法正常退出。 - Vladimir Kroz

3
您确定文件中的“行”是由换行符分隔的吗?

文件中可能就是问题所在。非常感谢。 - user431336

2
我有三个理论:
  • 输入文件不是UTF-8,而是一些无法确定的二进制格式,在读取为UTF-8时导致行非常长。

  • 该文件包含一些极长的“行”…或根本没有换行符。

  • 在您未向我们展示的代码中发生了其他事情;例如,您正在向set添加新元素。


为了帮助诊断问题:

  • 使用像od这样的工具(在UNIX / LINUX上)确认输入文件确实包含有效的行终止符;即CR、NL或CR NL。
  • 使用某些工具检查文件是否为有效的UTF-8。
  • 在您的代码中添加静态行计数器,并在应用程序因OOME而崩溃时打印出行计数器的值。
  • 跟踪到目前为止看到的最长行,并在出现OOME时也打印出来。

记录一下,您略微次优的trim使用与此问题无关。


1

可能的一种情况是在垃圾回收期间,你的堆空间不足。Hotspot JVM默认使用并行收集器,这意味着你的应用程序可能比垃圾回收器回收得更快。我曾经通过快速分配和丢弃对象,在仅有10K个活动(小)对象时引发OutOfMemoryError。

你可以尝试使用旧版本(pre-1.5)的串行收集器,并使用选项-XX:+UseSerialGC。还有其他几个"扩展"选项可用于调整回收。


-1

你可能想尝试将 String[] fields 的声明移出循环。因为你每次循环都在创建一个新的数组。你可以重复使用旧的数组,对吧?


1
他并没有创造任何东西。他只是声明了一个变量,用来保存一个String对象数组的引用(由split()返回)。由于它的作用范围仅限于循环内部,所以在那里声明它是完全可以的。 - Brian Roach
String[]是循环范围内的局部变量,数组分配的任何内存都将由JVM进行垃圾回收。 - bstick12
@BrianRoach 如果我错了,请纠正我,但是每次调用split()时都会创建一个String[],对吗?我理解@Shaunak的想法 - 如果在每个循环中都创建(和GC)一个String[],那么在循环之前声明它并在每次迭代中重复使用它,然后在循环结束后将其设置为null(以进行GC),这样不是更有效率吗?(我确定这就是在J2ME时代教授的方法!...) - ban-geoengineering

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