将文件读入字节数组时出现java.lang.OutOfMemoryError

9
有没有更干净、更快的方法来做这件事?
BufferedReader inputReader = new BufferedReader(new InputStreamReader(context.openFileInput("data.txt")));
String inputString;
StringBuilder stringBuffer = new StringBuilder();
while ((inputString = inputReader.readLine()) != null) {
    stringBuffer.append(inputString + "\n");
}
text = stringBuffer.toString();
byte[] data = text.getBytes();

基本上我正在尝试将一个文件转换成byte[],但是如果文件足够大,则会遇到内存不足错误。我在SO上寻找解决方案,我试图在这里做到这一点,但没有成功。任何帮助都将不胜感激。


1
那个帖子中有很多关于这个问题的好想法。 - keyser
我尝试实现实际答案,唯一的问题是我该如何处理mbb?像这样是否已经在byte []中了? - eWizardII
将一个 StringBuilder 命名为 StringBuffer 是令人困惑的,因为 StringBuffer 是 StringBuilder 的线程安全版本。只是说一下。 - keyser
7个回答

6
一些建议:
  1. 您不需要创建字符串生成器。您可以直接从文件中读取字节。
  2. 如果您读取多个文件,请检查这些byte[]数组是否在不需要时仍然保留在内存中。
  3. 最后,使用-Xmx选项增加Java进程的最大内存。

非常感谢您的建议。基本上,如果有文件上传,那么就清除旧文件,并继续这样做,以避免文件过大。 - eWizardII
3 是错误的建议。有些情况下,增加最大堆大小实际上会导致 OutOfMemoryError 错误。这里有一个很好的例子。 我也看到过 Oracle JDK 出现这种情况。我认为这与使用 -Xmx 增加最大堆大小时,同时减少了可用的本机内存有关,而 FileInputStream 使用本机内存,尽管这只是一种理论。-Xmx 标志仅增加最大堆大小,而不是您所说的“Java 进程的最大内存”,对于 32 位 Java,该值限制为 4GB。 - Max

3

正如我们所知,如果直接分配给定大小的字节数组而不是扩展它,可以节省大约一半的内存:

byte [] data = new byte[ (int) file.length() ];
FileInputStream fin = new FileInputStream(file);
int n = 0;
while ( (n = fin.read(data, n, data.length() - n) ) > 0);

这将避免分配不必要的附加结构。字节数组仅被分配一次,并且从一开始就具有正确的大小。while循环确保加载所有数据(read(byte[], offset, length)可能只读取文件的一部分,但返回读取的字节数)。
澄清:当StringBuilder用完时,它会分配一个新的缓冲区,该缓冲区比初始缓冲区大两倍。此时,我们使用的内存量约为最小需求的两倍。在最恶劣的情况下(最后一个字节无法适应某些已经很大的缓冲区),可能需要近三倍的最小内存量。

当StringBuilder用完时,它会分配一个新的缓冲区。此时,我们有两个缓冲区,旧的和新的。因此,在这一点上,我们使用的内存是最小要求的两倍。 - Audrius Meškauskas
我也认为新缓冲区的大小会是旧缓冲区的两倍(它肯定更大,对吧?:p)。文档没有明确说明这一点。 - keyser
是的,很可能是(old_size + 1)* 2,可以在OpenJDK的源代码中进行验证。因此,在最极端的情况下,可能需要多达三倍的内存才能满足要求。 - Audrius Meškauskas
1
我尝试实现这个解决方案,目前正在测试中。我想你的意思是fin.read而不是file.read。感谢您的帮助。 - eWizardII
谢谢,这似乎运行得相当不错。虽然我会继续测试它,但唯一可能遇到的问题是它只读取文件的一部分 - 但我会看看这是否是我的其他代码中的故障。 - eWizardII
显示剩余3条评论

2

如果你没有足够的内存来存储整个文件,你可以尝试重新考虑算法,在读取文件数据时进行处理,而不是构造大型byte[]数组数据。

如果你已经尝试通过调整-Xmx参数来增加Java内存,那么就没有任何解决方案可以让你将无法存放在内存中的数据存储在内存中。


0

你正在将字节复制到 char(使用了两倍的空间),然后再将其转换回字节。

InputStream in = context.openFileInput("data.txt");
ByteArrayOutputStream bais = new ByteArrayOutputStream();
byte[] bytes = new byte[8192];
for(int len; (lne = in.read(bytes) > 0;)
   bais.write(bytes, 0, len);
in.close();
return bais.toByteArray();

这将减少一半的内存需求,但仍可能导致内存不足。在这种情况下,您必须:

  • 增加最大堆大小
  • 逐步处理文件,而不是一次性全部处理
  • 使用内存映射文件,可以让您"加载"文件而不使用太多堆。

你指的是ByteArrayOutputStream,但它仍然不能解决问题。 - user207421
@EJP 正确。你可能错过了 ...但这仍然可能意味着你的内存耗尽了。在这种情况下,你必须要么... - Peter Lawrey

0

这类似于 Java中的文件转换为byte[]

您当前正在以字节形式阅读,将其转换为字符,然后尝试再次将其转换回字节。从Java API中的InputStreamReader类:

InputStreamReader是从字节流到字符流的桥梁:它读取字节并将其解码为字符。

直接读取字节会更有效率。

一种方法是直接在 context.openFileInput() 上使用ByteArrayInputStream,或者使用Jakarta Commons的IOUtils.toByteArray(InputStream),如果您使用JDK7,则可以使用Files.readAllBytes(Path)


-1
“更干净和更快的方法”是根本不去做它。这种方式无法扩展。应该逐个处理文件片段。

-2

这个解决方案将在加载之前测试可用内存...

File test = new File("c:/tmp/example.txt");

    long freeMemory = Runtime.getRuntime().freeMemory();
    if(test.length()<freeMemory) {
        byte[] bytes = new byte[(int) test.length()];
        FileChannel fc = new FileInputStream(test).getChannel();
        MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_ONLY, 0, (int) fc.size());

        while(mbb.hasRemaining()) {
            mbb.get(bytes);
        }
        fc.close();
    }

那么如果内存不足,他该怎么办?这不是一个答案。 - user207421
如果内存不足,则无法完成,问题中所述的要求是拥有一个包含整个文件内容的字节数组!是的,我同意您的帖子,如果可能的话,应该流式处理和分块处理。 - JayTee

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