加密期间Java内存不足错误

时间:2018-03-12 12:37:20

标签: java encryption out-of-memory byte aes

我使用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()将文件作为输入并返回字节数组

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

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)

如果我使用较小尺寸的文件,我没有收到此错误,但是再一次,使用缓冲区的唯一目的是消除内存不足问题。

提前致谢。任何帮助表示赞赏。

3 个答案:

答案 0 :(得分:1)

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

实际上,您在ByteArrayOutputStream baos中添加了块,这意味着它必须在内部存储它,然后才能在其上调用toByteArray()。所以它是你的数据的5份副本,意思是6GB +。 ByteArrayOutputStream在内部使用数组,因此它与ArrayList类似地增长,因此它将使用比所需更多的内存(请参阅stacktrace - 它尝试调整大小)。

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

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

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

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

答案 1 :(得分: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 block [0]
  • Cipher.update block [1]
  • Cipher.update block [2]
  • ...
  • Cipher.doFinal(块[N-1])

或者代替最后一个doFinal:

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

每个updatedoFinal产生一部分加密数据。

doFinal也“刷新”最终加密数据。

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

即可
byte[] encryptedBlock = cipher.doFinal(plainBlock);

然后不需要拨打cipher.update

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

而不是File新版Path更具通用性,并与Paths.get("...")非常好实用程序类Files结合使用可以提供强大的代码:如Files.readAllBytes(path)等等。

答案 2 :(得分:0)

在迭代文件时,请保留一个计数器来跟踪字节数:

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;
}

然后使用带有size参数的构造函数来创建输出缓冲区:

ByteArrayOutputStream baos = new ByteArrayOutputStream(encryptedBytesSize);

这将避免内部缓冲区不得不增长。增长可能是非线性的,因此每次迭代添加更多字节,下次增长时会分配更多空间。

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

  1. 阅读未加密文件的一小部分
  2. 加密块
  3. 写入加密文件
  4. 这样可以避免同时在内存中存储所有常规和加密文件。