Java 加密期间出现内存不足错误

Java Out of Memory Error during Encryption

我正在使用 AES 加密文件。当我尝试加密一个大文件时,问题首先出现。所以我做了一些在线阅读并认为我需要使用缓冲区并且一次只加密数据字节。

我将明文分成 8192 字节的数据块,然后对每个数据块应用加密操作,但我仍然遇到内存不足错误。

public static  File encrypt(File f, byte[] key) throws Exception
{
    System.out.println("Starting Encryption");
    byte[] plainText = fileToByte(f);
    SecretKeySpec secretKey = new SecretKeySpec(key, ALGORITHM);
    Cipher cipher = Cipher.getInstance(ALGORITHM);
    cipher.init(Cipher.ENCRYPT_MODE, secretKey);

    System.out.println(plainText.length);

    List<byte[]> bufferedFile = divideArray(plainText, 8192);


    System.out.println(bufferedFile.size());

    List<byte[]> resultByteList = new ArrayList<>();

    for(int i = 0; i < bufferedFile.size(); i++)
    {
        resultByteList.add(cipher.doFinal(bufferedFile.get(i)));
    }

    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    for(byte[] b : resultByteList)
        baos.write(b);

    byte[] cipherText = baos.toByteArray();

    File temp = byteToFile(cipherText, "D:\temp");

    return temp;

}

fileToByte() 将一个文件作为输入,returns 一个字节数组

divideArray()将一个字节数组作为输入,并将其分成一个由更小的字节数组组成的数组列表。

public static List<byte[]> divideArray(byte[] source, int chunkSize) {

    List<byte[]> result = new ArrayList<byte[]>();
    int start = 0;
    while (start < source.length) {
        int end = Math.min(source.length, start + chunkSize);
        result.add(Arrays.copyOfRange(source, start, end));
        start += chunkSize;
    }

    return result;
}

这是我得到的错误

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3236)
at java.io.ByteArrayOutputStream.grow(ByteArrayOutputStream.java:118)
at java.io.ByteArrayOutputStream.ensureCapacity(ByteArrayOutputStream.java:93)
at java.io.ByteArrayOutputStream.write(ByteArrayOutputStream.java:153)
at java.io.OutputStream.write(OutputStream.java:75)
at MajorProjectTest.encrypt(MajorProjectTest.java:61)
at MajorProjectTest.main(MajorProjectTest.java:30)

如果我使用较小的文件,我不会收到此错误,但话又说回来,使用缓冲区的唯一目的是消除内存不足问题。

提前致谢。感谢任何帮助。

在遍历文件时,保留一个计数器来跟踪字节数:

int encryptedBytesSize = 0;
for(int i = 0; i < bufferedFile.size(); i++) {
    resultByteList.add(cipher.doFinal(bufferedFile.get(i)));
    encryptedBytesSize += resultByteList.get(resultByteList.size() - 1).length;
}

然后使用带大小参数的构造函数创建输出缓冲区:

ByteArrayOutputStream baos = new ByteArrayOutputStream(encryptedBytesSize);

这将避免内部缓冲区变大。增长可能是 non-linear,因此随着每次迭代添加更多字节,甚至在下一次增长时分配更多 space。

但这仍然可能不起作用,具体取决于文件大小。另一种方法是:

  1. 读取一小块未加密文件
  2. 加密块
  3. 写入加密文件

这避免了内存中同时存在所有常规文件和加密文件。

查看这四个变量:plainTextbufferedFileresultByteListcipherText。它们都以略有不同的格式包含您的整个文件,这意味着它们每个都是 1.2GB 大。其中两个是 Lists 这意味着它们可能更大,因为您没有设置 ArrayLists 的初始大小并且它们会在需要时自动调整大小。所以我们说的是需要超过 5GB 的内存。

实际上,你将块添加到 ByteArrayOutputStream baos,这意味着它必须在你调用 toByteArray() 之前在内部存储它。所以它是你数据的 5 个副本,意味着 6GB+。 ByteArrayOutputStream 在内部使用数组,因此它的增长与 ArrayLists 类似,因此它将使用比需要更多的内存(请参阅堆栈跟踪 - 它试图调整大小)。

所有这些变量都在同一个范围内,从不分配null这意味着它们不能被垃圾收集。


您可以增加最大堆限制(参见 Increase heap size in Java),但这将严重限制您的程序。

您的程序在写入 ByteArrayOutputStream 时抛出内存不足错误。这是您第 4 次复制所有数据,这意味着已经分配了 3.6GB。由此我推断您的堆设置为 4GB(这是您可以在 32 位操作系统上设置的最大值)。


你应该做的是有一个循环,读取文件的一部分,加密它并写入另一个文件。这将避免将整个文件加载到内存中。 List<byte[]> bufferedFile = divideArray(plainText, 8192);resultByteList.add(...) 之类的行是您不应该在代码中包含的内容 - 您最终会将整个文件存储在内存中。您唯一需要跟踪的是游标(即表示您已经处理了哪些字节的位置),这是 O(1) 内存复杂度。然后你只需要和你正在编码的块一样多的内存——这比整个文件小得多。

一个问题是在内存中保存数组和数组副本。

块读写。

那么doFinal就不要重复了。请改用 update。许多例子只使用一个单一的 doFinal,这是误导。

所以:

public static  File encrypt(File f, byte[] key) throws Exception
{
    System.out.println("Starting Encryption");
    SecretKeySpec secretKey = new SecretKeySpec(key, ALGORITHM);
    Cipher cipher = Cipher.getInstance(ALGORITHM);
    cipher.init(Cipher.ENCRYPT_MODE, secretKey);

    System.out.println(plainText.length);

    Path outPath = Paths.get("D:/Temp");
    byte[] plainBuf = new byte[8192];
    try (InputStream in = Files.newInputStream(f.toPath());
            OutputStream out = Files.newOutputStream(outPath)) {
        int nread;
        while ((nread = in.read(plainBuf)) > 0) {
            byte[] enc = cipher.update(plainBuf, 0, nread);
            out.write(enc);
        }       
        byte[] enc = cipher.doFinal();
        out.write(enc);
    }
    return outPath.toFile();
}

说明

一些字节块的加密如下:

  • Cipher.init
  • Cipher.update块[0]
  • Cipher.update块[1]
  • Cipher.update块[2]
  • ...
  • Cipher.doFinal(块[n-1])

或者代替最后一个doFinal:

  • Cipher.update(块[n-1])
  • Cipher.doFinal()

updatedoFinal 产生一部分加密数据。

doFinal也"flushes"最终加密数据

如果只有一个字节块,调用

就足够了
byte[] encryptedBlock = cipher.doFinal(plainBlock);

那么就不需要调用 cipher.update

对于其余部分,我使用了 try-with-resources 自动关闭输入和输出流的语法,即使发生 return 或抛出异常也是如此。

而不是 File,较新的 Path 更通用一些,并且结合 Paths.get("...")非常好 实用程序 class Files 可以提供强大的代码:比如 Files.readAllBytes(path) 等等。