将文件编码为base64时出现内存不足错误

17

使用Apache commons中的Base64

public byte[] encode(File file) throws FileNotFoundException, IOException {
        byte[] encoded;
        try (FileInputStream fin = new FileInputStream(file)) {
            byte fileContent[] = new byte[(int) file.length()];
            fin.read(fileContent);
            encoded = Base64.encodeBase64(fileContent);
        }
        return encoded;   
}


Exception in thread "AWT-EventQueue-0" java.lang.OutOfMemoryError: Java heap space
    at org.apache.commons.codec.binary.BaseNCodec.encode(BaseNCodec.java:342)
    at org.apache.commons.codec.binary.Base64.encodeBase64(Base64.java:657)
    at org.apache.commons.codec.binary.Base64.encodeBase64(Base64.java:622)
    at org.apache.commons.codec.binary.Base64.encodeBase64(Base64.java:604)

我正在制作一个针对移动设备的小应用程序。


似乎你没有足够的堆空间... :) - aviad
file.length()有多大?看起来太大了 :) - Corbin
1
@Ivan:那你当时期望什么呢? - Michael Borgwardt
@aviad:-Xmx500M?如果移动设备上的JVM甚至没有该选项,那是行不通的。 - Michael Borgwardt
@aviad:我不认为将堆大小增加到半GB对于一个只需要1K(但写得很差)的程序是一个好建议。 - Tomasz Nurkiewicz
@MichaelBorgwardt,我太傻了 :) 没看到“移动”的字眼。@Tomasz Nurkiewicz,同意。 - aviad
8个回答

35

你不能像这样将整个文件加载到内存中:

byte fileContent[] = new byte[(int) file.length()];
fin.read(fileContent);

可以逐块加载文件并分段进行编码。Base64是一种简单的编码方式,每次加载3个字节并进行编码就足够了(这将在编码后产生4个字节)。出于性能考虑,考虑加载3倍数的字节,例如3000字节-应该就可以了。还要考虑缓冲输入文件。

一个例子:

byte fileContent[] = new byte[3000];
try (FileInputStream fin = new FileInputStream(file)) {
    while(fin.read(fileContent) >= 0) {
         Base64.encodeBase64(fileContent);
    }
}

请注意,您不能简单地将Base64.encodeBase64()的结果附加到encoded字节数组中。实际上,这不是加载文件而是将其编码为Base64,导致了内存不足的问题。这是可以理解的,因为Base64版本更大(而且您已经有一个占用大量内存的文件)。

考虑更改您的方法为:

public void encode(File file, OutputStream base64OutputStream)

直接将Base64编码的数据发送到 base64OutputStream 而不是返回它。

更新:由于@StephenC的帮助,我开发了一个更简单的版本:

public void encode(File file, OutputStream base64OutputStream) {
  InputStream is = new FileInputStream(file);
  OutputStream out = new Base64OutputStream(base64OutputStream)
  IOUtils.copy(is, out);
  is.close();
  out.close();
}

它使用Base64OutputStream将输入即时翻译为Base64,同时使用IOUtils类来处理缓冲,此类源自Apache Commons IO

注意:如果需要打印=,则必须显式关闭FileInputStreamBase64OutputStream,但缓冲由IOUtils.copy()处理。


大多数Base64的变体比这个更复杂...以应对固定行长度的要求。请参阅我的答案以了解替代方案。 - Stephen C
@IvanIvanovich:这就是我想说的!如果你有一个相当大的文件(比如100 MiB),你首先要将其加载到内存中,然后将其Base64编码为byte[],这总共需要 ~233 MiB 的空间 - 如果你使用流式传输,可能只需要几个 KiB 就足够了。如果你真的需要一个byte[],可以考虑使用ByteArrayOutputStream,但你又会遇到OOM问题 - 这正是我们试图避免的。 - Tomasz Nurkiewicz
@IvanIvanovich:没问题,我们在这里尽力帮忙。看一下这个链接:http://www.coderanch.com/t/275464/Streams/java/OutputStream-InputStream 和 https://dev59.com/_HM_5IYBdhLWcg3wymY0 - Tomasz Nurkiewicz
1
我在第一种解决方案中尝试了这个:sb.append(Base64.encodeToString(fileContent, Base64.DEFAULT)); 并且它对我有效。其中sb是StringBuilder对象。 - Arun Badole
FileInputStream不能保证读取3000个字节。看起来我们需要使用DataInputStream.readFully()方法。否则,Base64编码器将提供不同的输出。 - Oleksandr Albul
显示剩余9条评论

6

不要一次对整个文件进行操作。

Base64 每次处理 3 个字节,所以您可以将文件分批读取,每批包含“3 的倍数”个字节,进行编码并重复执行,直到完成整个文件:

// the base64 encoding - acceptable estimation of encoded size
StringBuilder sb = new StringBuilder(file.length() / 3 * 4);

FileInputStream fin = null;
try {
    fin = new FileInputStream("some.file");
    // Max size of buffer
    int bSize = 3 * 512;
    // Buffer
    byte[] buf = new byte[bSize];
    // Actual size of buffer
    int len = 0;

    while((len = fin.read(buf)) != -1) {
        byte[] encoded = Base64.encodeBase64(buf);

        // Although you might want to write the encoded bytes to another 
        // stream, otherwise you'll run into the same problem again.
        sb.append(new String(buf, 0, len));
    }
} catch(IOException e) {
    if(null != fin) {
        fin.close();
    }
}

String base64EncodedFile = sb.toString();

如何反转这个过程,也就是从base64格式获取原始文件? - Vishal Senjaliya
1
在Java 8中,您可以使用wrap方法将base64编码的InputStream包装起来,以读取解码后的数据。 - Sorin

6
要么文件太大,要么堆太小,要么存在内存泄漏问题。
  • 如果只在处理非常大的文件时出现此问题,请在代码中添加检查文件大小并拒绝不合理大的文件的逻辑。
  • 如果出现在处理小文件时,请在启动JVM时使用-Xmx命令行选项来增加堆大小。(如果是在Web容器或其他框架中运行,请检查相关文档以获取更多信息)
  • 如果该文件反复出现,尤其是小文件,则很可能存在内存泄漏问题。

另外需要指出的一点是,你目前的方法涉及在内存中保持两个完整的文件副本。你应该能够减少内存使用量,但通常需要使用基于流的Base64编码器。(这取决于使用的base64编码的类型...) 此页面描述了一种基于流的Base64编码器/解码器库,并包括一些替代品链接。

1
+1,你的回答让我发现了Commons Codec中的Base64OutputStream,这似乎是最好的解决方案。 - Tomasz Nurkiewicz

1
1. 你并没有读取整个文件,只是前几KB。read方法会返回实际读取的字节数。你应该在循环中调用read方法,直到它返回-1,以确保你已经读取了所有内容。
2. 文件太大,无法同时放入内存和base64编码中。可以将文件分成较小的部分进行处理,或者使用-Xmx开关增加JVM可用的内存,例如:
java -Xmx1024M YourProgram

1

这是上传更大图片的最佳代码

bitmap=Bitmap.createScaledBitmap(bitmap, 100, 100, true);

ByteArrayOutputStream stream = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream); //compress to which format you want.
byte [] byte_arr = stream.toByteArray();  
String image_str = Base64.encodeBytes(byte_arr);

0
在应用程序标签的清单文件中写入以下内容: android:largeHeap="true"
这对我很有效。

0

看起来你的文件太大了,无法在可用堆内存中保留多个副本以进行内存中Base64编码。考虑到这是针对移动设备的,可能无法增加堆大小,因此你有两个选择:

  • 使文件变小(远远小于现在)
  • 以流的方式进行操作,这样你就可以一次从InputStream读取文件的一小部分,对其进行编码并将其写入OutputStream,而不必将整个文件保存在内存中。

@Ivan:这个网站的目的是回答问题,而不是让别人为你编写代码。 - Michael Borgwardt

0
Java 8增加了Base64方法,因此不再需要使用Apache Commons来编码大文件。
public static void encodeFileToBase64(String inputFile, String outputFile) {
    try (OutputStream out = Base64.getEncoder().wrap(new FileOutputStream(outputFile))) {
        Files.copy(Paths.get(inputFile), out);
    } catch (IOException e) {
        throw new UncheckedIOException(e);
    }
}

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