Java中的OutOfMemoryError与FileOutputStream有关吗?

4

感谢大家^_^,问题已解决:有一个单独的行太长(超过400M...我下载了一个损坏的文件,而我没有意识到),所以抛出了OutOfMemoryError

我想使用Java拆分文件,但它总是抛出OutOfMemoryError:Java堆空间,我在整个互联网上搜索,但好像没有帮助:(

附言。该文件的大小为600M,有超过30,000,000行,每行不超过100个字符。 (也许您可以生成一个像这样的“级别文件”: id:0000000001,级别:1 id:0000000002,级别:2 ....(超过30百万) )

附言2。设置更大的Jvm内存大小无效,:(

附言3。我换了一台电脑,问题仍然存在/(ㄒoㄒ)/~~

无论我设置多大的-Xms或-Xmx,输出文件的大小总是相同的(而且Runtime.getRuntime().totalMemory()确实改变了)

以下是堆栈跟踪:

 Heap Size = 2058027008
    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:515)
        at java.lang.StringBuffer.append(StringBuffer.java:306)
        at java.io.BufferedReader.readLine(BufferedReader.java:345)
        at java.io.BufferedReader.readLine(BufferedReader.java:362)
        at com.xiaomi.vip.tools.ptupdate.updator.Spilt.main(Spilt.java:39)
    ...

这是我的代码:

package com.updator;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;

public class Spilt {
    public static void main(String[] args) throws Exception {
        long heapSize = Runtime.getRuntime().totalMemory();

        // Print the jvm heap size.
        System.out.println("Heap Size = " + heapSize);

        String mainPath = "/home/work/bingo/";
        File mainFilePath = new File(mainPath);
        FileInputStream inputStream = null;
        FileOutputStream outputStream = null;
        try {
            if (!mainFilePath.exists())
                mainFilePath.mkdir();

            String sourcePath = "/home/work/bingo/level.txt";
            inputStream = new FileInputStream(sourcePath);
            BufferedReader bufferedReader = new BufferedReader(new FileReader(
                    new File(sourcePath)));

            String savePath = mainPath + "tmp/";
            Integer i = 0;
            File file = new File(savePath + "part"
                    + String.format("%0" + 5 + "d", i) + ".txt");
            if (!file.getParentFile().exists())
                file.getParentFile().mkdir();
            file.createNewFile();
            outputStream = new FileOutputStream(file);
            int count = 0, total = 0;
            String line = null;
            while ((line = bufferedReader.readLine()) != null) {
                line += '\n';
                outputStream.write(line.getBytes("UTF-8"));
                count++;
                total++;
                if (count > 4000000) {
                    outputStream.flush();
                    outputStream.close();
                    System.gc();
                    count = 0;
                    i++;
                    file = new File(savePath + "part"
                            + String.format("%0" + 5 + "d", i) + ".txt");
                    file.createNewFile();
                    outputStream = new FileOutputStream(file);
                }
            }

            outputStream.close();
            file = new File(mainFilePath + "_SUCCESS");
            file.createNewFile();
            outputStream = new FileOutputStream(file);
            outputStream.write(i.toString().getBytes("UTF-8"));
        } finally {
            if (inputStream != null)
                inputStream.close();
            if (outputStream != null)
                outputStream.close();
        }
    }
}

我认为可能是:当outputStream.close()时,内存没有释放?

1
为什么要使用 Scanner?你不需要这个功能,一个 BufferedReader 就足够了,而且更节省资源。 - piet.t
@piet.t 我认为问题不在于Scanner(我已经改用BufferedReader,问题仍然存在 :( ) - ACBingo
实际上,这行代码不超过100个字符。我认为当outputStream.close()时,outputStream的内存没有被清除... - ACBingo
2
堆栈信息很清晰:bufferedReader.readLine 抛出了 outOfMemory 异常。最直接的原因是:有一行文本无法放入内存中。(你可以使用 System.out.println 输出行数来查看哪一行)。 - GPI
显示剩余10条评论
2个回答

3

你打开原始文件,并创建一个BufferedReader和一个行计数器。

char[] buffer = new char[5120];
BufferedReader reader = Files.newBufferedReader(Paths.get(sourcePath), StandardCharsets.UTF_8);
int lineCount = 0;

现在您可以读取缓冲区,并在字符写入时将其写入。

int read;

BufferedWriter writer = Files.newBufferedWriter(Paths.get(fileName), StandardCharsets.UTF_8);
while((read = reader.read(buffer, 0, 5120))>0){
    int offset = 0;
    for(int i = 0; i<read; i++){
        char c = buffer[i];
        if(c=='\n'){
           lineCount++;
           if(lineCount==maxLineCount){
              //write the range from 0 to i to your old writer.
              writer.write(buffer, offset, i-offset);
              writer.close();
              offset=i;
              lineCount=0;
              writer = Files.newBufferedWriter(Paths.get(newName), StandarCharset.UTF_8);
           }
        }
        writer.write(buffer, offset, read-offset);
    }
    writer.close();
}

这样可以保持内存使用率较低,并防止一次读取太大的行。您可以不使用BufferedWriters来更好地控制内存,但我认为这并不是必要的。


为什么是5120呢?读取5120个字节的原因是什么?我的意思是,如果一行只有100个字符,那不应该更糟糕吗? - ACBingo
5120 是一个缓冲区大小,我只是随意挑选的。由于正在使用缓冲读取器,所以并不重要,即使每次只读取一个字符也可以正常工作。你为什么认为它在长度为100的行上表现会更差? - matt

1

我已经测试过大文本文件(250Mb)。

它运行得很好。

你需要为文件流添加try catch异常代码。

public class MyTest {
    public static void main(String[] args) {
        String mainPath = "/home/work/bingo/";
        File mainFilePath = new File(mainPath);
        FileInputStream inputStream = null;
        FileOutputStream outputStream = null;
        try {
            if (!mainFilePath.exists())
                mainFilePath.mkdir();

            String sourcePath = "/home/work/bingo/level.txt";
            inputStream = new FileInputStream(sourcePath);
            Scanner scanner = new Scanner(inputStream, "UTF-8");

            String savePath = mainPath + "tmp/";
            Integer i = 0;
            File file = new File(savePath + "part" + String.format("%0" + 5 + "d", i) + ".txt");
            if (!file.getParentFile().exists())
                file.getParentFile().mkdir();
            file.createNewFile();
            outputStream = new FileOutputStream(file);
            int count = 0, total = 0;

            while (scanner.hasNextLine()) {
                String line = scanner.nextLine() + "\n";
                outputStream.write(line.getBytes("UTF-8"));
                count++;
                total++;
                if (count > 4000000) {
                    outputStream.flush();
                    outputStream.close();
                    count = 0;
                    i++;
                    file = new File(savePath + "part" + String.format("%0" + 5 + "d", i) + ".txt");
                    file.createNewFile();
                    outputStream = new FileOutputStream(file);
                }
            }

            outputStream.close();
            file = new File(mainFilePath + "_SUCCESS");
            file.createNewFile();
            outputStream = new FileOutputStream(file);
            outputStream.write(i.toString().getBytes("UTF-8"));
        } catch (FileNotFoundException e) {
            System.out.println("ERROR: FileNotFoundException :: " + e.getStackTrace());
        } catch (IOException e) {
            System.out.println("ERROR: IOException :: " + e.getStackTrace());
        } finally {
            if (inputStream != null)
                try {
                    inputStream.close();
                    if (outputStream != null)
                        outputStream.close();

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

如果问题仍然存在,请在shell提示符下使用以下命令更改Java堆内存大小。
例如:Xmx1g表示1GB堆内存大小,MyTest表示类名。
java -Xmx1g MyTest

我已经尝试了一下2GB的文本文件,但是没有出现任何问题。 我的系统环境:Intel i5 / Java 1.7 / 6GB内存。 - Smith Lee
如果您的系统内存非常小,请减少代码行数。例如,将4000000缩减为400。 - Smith Lee
我尝试了,问题仍然存在 :( 附注:我的环境是i7/Java1.6/16GB内存,并且输出文件的总大小也相同。 - ACBingo
非常感谢...但更改堆内存不起作用.... /(ㄒoㄒ)/~~ - ACBingo

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