我使用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)
如果我使用较小尺寸的文件,我没有收到此错误,但是再一次,使用缓冲区的唯一目的是消除内存不足问题。
提前致谢。任何帮助表示赞赏。
答案 0 :(得分:1)
查看以下四个变量:plainText
,bufferedFile
,resultByteList
,cipherText
。所有这些文件都以稍微不同的格式包含整个文件,这意味着它们每个都是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();
}
<强> 解释 强>
某些字节块的加密如下:
或者代替最后一个doFinal:
每个update
或doFinal
产生一部分加密数据。
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);
这将避免内部缓冲区不得不增长。增长可能是非线性的,因此每次迭代添加更多字节,下次增长时会分配更多空间。
但这仍然可能不起作用,具体取决于文件大小。另一种方法是:
这样可以避免同时在内存中存储所有常规和加密文件。