代码之家  ›  专栏  ›  技术社区  ›  Neeraj Athalye

加密期间发生Java内存不足错误

  •  0
  • Neeraj Athalye  · 技术社区  · 6 年前

    我正在使用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 回复  |  直到 6 年前
        1
  •  2
  •   Joop Eggen    6 年前

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

    读写块。

    然后 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();
    }
    

    解释

    某些字节块的加密如下:

    • 密码初始化
    • 密码更新块[0]
    • 密码更新块[1]
    • 密码更新块[2]
    • 。。。
    • 密码doFinal(区块【n-1】)

    或者代替最后一个doFinal:

    • 密码更新(块[n-1])
    • 密码doFinal()

    每一个 使现代化 doFinal公司 产生部分加密数据。

    doFinal公司 还“刷新”最终加密数据。

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

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

    那么就不要打电话给 cipher.update 都是必需的。

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

    而不是 File 更新的 Path 功能更加全面,与 Paths.get("...") 以及 非常好 实用程序类 Files 可以提供强大的代码:如 Files.readAllBytes(path) 还有更多。

        2
  •  1
  •   Jaroslaw Pawlak    6 年前

    看看这四个变量: plainText ,则, bufferedFile ,则, resultByteList ,则, cipherText 。所有这些文件都以稍微不同的格式包含整个文件,这意味着每个文件的大小都是1.2GB。其中两个是 List 这意味着它们可能会更大,因为您没有设置 ArrayList s,并在需要时自动调整大小。因此,我们讨论的是需要5GB以上的内存。

    实际上,你在 ByteArrayOutputStream baos ,这意味着它必须在调用之前在内部存储 toByteArray() 在上面。因此,您的数据有5个拷贝,即6GB以上。这个 ByteArrayOutputStream 在内部使用数组,因此其增长类似于 阵列列表 因此它将使用比所需更多的内存(请参阅stacktrace-它试图调整大小)。

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


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

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


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

        3
  •  0
  •   Andrew S    6 年前

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

    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. 写入加密文件

    这样可以避免将所有常规文件和加密文件同时存储在内存中。