尝试读/写巨大文本文件时出现OutOfMemoryError错误

3

我试图读/写一个巨大的文本文件。 但是当我尝试这样做时,会出现以下错误:

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    at java.util.Arrays.copyOf(Unknown Source)
    at java.lang.AbstractStringBuilder.expandCapacity(Unknown Source)
    at java.lang.AbstractStringBuilder.append(Unknown Source)
    at java.lang.StringBuilder.append(Unknown Source)
    at ReadWriteTextFile.getContents(ReadWriteTextFile.java:52)
    at ReadWriteTextFile.main(ReadWriteTextFile.java:148)

我的代码如下:

import java.io.*;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;

public class ReadWriteTextFile {

  /**
  * Fetch the entire contents of a text file, and return it in a String.
  * This style of implementation does not throw Exceptions to the caller.
  *
  * @param aFile is a file which already exists and can be read.
  */    
  static public String getContents(File aFile) {
    //...checks on aFile are elided
      StringBuilder contents = new StringBuilder(); 
      int maxlines = 1000; //counts max lines t read/write to the file
      BufferedReader input = null;
      BufferedWriter bw = null;

    try {
      //use buffering, reading one line at a time
      //FileReader always assumes default encoding is OK!
      input =  new BufferedReader(new FileReader(aFile));
      try {
          String line = null; //not declared within while loop
        /*
        * readLine is a bit quirky :
        * it returns the content of a line MINUS the newline.
        * it returns null only for the END of the stream.
        * it returns an empty String if two newlines appear in a row.
        */
        //for (int i = 0; i < 100; i++){
        //int count = 0;//initiates the line counter
      while (( line = input.readLine()) != null){

          int count = 0;//initiates the line counter    
          String modified1 = line.substring(2,17);
          String modified2 = line.substring(18,33);
          String modified3 = line.substring(40);        
          String result = "empty";
          result = modified1 + ",," +modified2 + modified3;
          System.out.println (result);          

//        contents.append(line);
//        contents.append(System.getProperty("line.separator"));
          //int count = 0;//initiates the line counter
          try {

              contents.append(line);
              contents.append(System.getProperty("line.separator"));
          String content = result;

          File file = new File("C:\\temp\\out.txt");//output path

          // if file doesnt exists, then create it
          if (!file.exists()) {
          file.createNewFile();
          }
          for ( int i = 0; i < 1000; i++){
              if (count++ % maxlines == 0) {
          FileWriter fw = new FileWriter(file.getAbsoluteFile(),true);
          bw = new BufferedWriter(fw);      
              bw.write(content);
          bw.newLine(); 
          }
          bw.close();
          }

        } catch (IOException e) {
            e.printStackTrace();
        }

        //}
        }
      }
      finally {
          input.close();
          bw.close();

      }
    }
    catch (IOException ex){
        ex.printStackTrace();
    }

    return contents.toString();
  }


/**
  * Change the contents of text file in its entirety, overwriting any
  * existing text.
  *
  * This style of implementation throws all exceptions to the caller.
  *
  * @param aFile is an existing file which can be written to.
  * @throws IllegalArgumentException if param does not comply.
  * @throws FileNotFoundException if the file does not exist.
  * @throws IOException if problem encountered during write.
  */
  static public void setContents(File aFile, String aContents)
                                 throws FileNotFoundException, IOException {
    if (aFile == null) {
        throw new IllegalArgumentException("File should not be null.");
    }
    if (!aFile.exists()) {
        throw new FileNotFoundException ("File does not exist: " + aFile);
    }
    if (!aFile.isFile()) {
        throw new IllegalArgumentException("Should not be a directory: " + aFile);
    }
    if (!aFile.canWrite()) {
        throw new IllegalArgumentException("File cannot be written: " + aFile);
    }

    //use buffering
    Writer output = new BufferedWriter(new FileWriter(aFile, true));
    try {
      //FileWriter always assumes default encoding is OK!
        output.write( aContents );
    }
    finally {
      output.close();
    }

  }

  /** Simple test harness.   */
  public static void main (String... aArguments) throws IOException {
      File testFile = new File("C:\\temp\\in.txt");//input path
      System.out.println("\n" + getContents(testFile));

  }

}

我尝试添加计数器(count),以便在读取一定数量的行后刷新缓冲区。但它没有起作用。 我知道计数器不能正确工作。在“while”循环的特定执行次数之后,它不会归零。我在while循环之前和之后添加了一个“for”循环来清空计数器,但这也没有起作用。 有什么建议吗?

使用更高的堆大小启动Java应用程序,例如 java -Xms:4g myApp - user2511414
为什么不在循环之前打开输出文件? - Ingo
4个回答

8

尝试使用FileInputStream而不是BufferedReader/Writer。当我使用FileInputStream时,我可以在几秒钟内复制一个超过3600万行且大小接近500MB的虚拟日志文件。

FileInputStream in = new FileInputStream(from); //Read data from a file
FileOutputStream out = new FileOutputStream(to); //Write data to a file
byte[] buffer = new byte[4096]; //Buffer size, Usually 1024-4096
int len;
while ((len = in.read(buffer, 0, buffer.length)) > 0) {
    out.write(buffer, 0, len);
}
//Close the FileStreams
in.close();
out.close();

如果您想按行而不是按字节块读取文件,可以使用BufferedReader,但是需要以不同的方式使用。
// Removed redundant exists()/createNewFile() calls altogether
String line;
BufferedReader br = new BufferedReader(new FileReader(aFile));
BufferedWriter output = new BufferedWriter(new FileWriter(file, true));
while ((line = br.readLine()) != null) {
      String modified1 = line.substring(2,17);
      String modified2 = line.substring(18,33);
      String modified3 = line.substring(40); 
      String result = "empty";
      result = modified1 + ",," +modified2 + modified3;
      System.out.println (result);
      output.append(result + "\n");//Use \r\n for Windows EOL
}
//Close Streams
br.close();
output.close();

像EJP所说,不要将整个文件读入内存 - 这样做并不明智。你最好的选择是逐行读取每一行或者一次性读取文件的块 - 虽然为了准确性,逐行读取可能是最好的。
while ((line = br.readLine()) != null)期间,你应该做需要整个文件加载的事情,而只有1行被加载到内存中。(例如检查一行是否包含_或从中获取文本)。
另一件你可以尝试避免OOM异常的事情是使用多个字符串。
if(contents.length() => (Integer.MAX_VALUE-5000)) { //-5000 to give some headway when checking
    . . .
}

但是OOM错误是由于StringBuilder用完可用空间而导致的。 - bsd
@EJP 但这是写入文件。这是OP想要的吗? - bsd
OP的代码将数据写入文件。但是,为此它需要将整个文件加载到内存中,这并不是必要的。 - user207421
OP可能想要将每一行复制1000次到文件中,这样不会导致OOM错误。OP可能也想要文件的内容。上面的代码片段只是为了优化写入文件的操作,与OOM错误无关。 - bsd
我不知道他“可能想要”什么。你也不知道。他的代码实际上只是一次复制整个文件。他所有关于maxlines等的东西只是一些可能不起作用的废话,只是计数到1000然后写入文件。这个答案中的代码完全做到了OP的代码实际上所做的事情,而没有内存问题。 - user207421
@EJP 不要混淆contentcontents,我也曾经犯过这个错误。 - Ingo

0
我尝试添加一个计数器(count),以便在读取一定数量的行后刷新缓冲区。但它没有起作用。我知道计数器不正确地工作了。它在执行“while”循环的特定次数后不会归零。我在while循环之前和之后添加了一个“for”循环来清空计数器,但也没有起作用。
你遇到的内存不足错误是因为文件太大,无法将该文件的所有内容读入函数getContents(File aFile)中的本地变量contents中。
刷新缓冲区与此无关。使用PrintWriter而不是BufferedWriter可能有助于清理代码。通过使用PrintWriter,您不必像这样做:
bw.write(content);
bw.newLine(); 

你可以将此更改为:

printWriter.println(content);

你还忘了告诉我们你的使用情况。最后,你所做的只是打印文件的所有内容。你本可以逐行完成这个任务。

很难看出使用PrintWriter如何对内存问题产生任何影响。 - user207421
我的意思是这是文本 I/O。PrintWriter 已经有很多好处了。为什么不使用它们? - bsd
首先,因为它会吞掉异常。这与所提出的问题无关。你回答中唯一相关的部分是最后一句话。 - user207421

0
不要试图将大文件读入内存,它们无法适应。找到一种逐行、逐个记录或逐块处理文件的方法。我看不出为什么你不能这样做。
在构建围绕同一文件的FileWriter之前立即调用File.exists()和File.createNewFile()是完全浪费时间的。

@downvoter 请解释一下。逐行读取文件无法解决这个问题吗?不可能吗?在新建FileWriter()之前,exists()和createNewFile()不是多余的吗?到底是哪一个呢?未经解释的负评对任何人都没有帮助,而且很可能被视为纯粹的网站破坏行为。 - user207421

0
在Java中读取大文件,您应该使用java.util.scanner或apache commons LineIterator。这两种方法都不会将整个文件加载到内存中,而是逐行读取文件。我能够使用LineIterator读取大小超过1GB的文件。请访问此链接http://www.baeldung.com/java-read-lines-large-file以获取更多详细信息和示例。

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