使用SharpZipLib压缩大文件导致内存不足异常

3
我有一个453MB的XML文件,我试图使用SharpZipLib将其压缩成ZIP文件。 以下是我用来创建zip文件的代码,但是它会引起OutOfMemoryException异常。这段代码成功地压缩了一个428MB的文件。 你有任何想法为什么会出现异常吗?因为我看不出原因,我的系统拥有充足的内存。
public void CompressFiles(List<string> pathnames, string zipPathname)
{
    try
    {
        using (FileStream stream = new FileStream(zipPathname, FileMode.Create, FileAccess.Write, FileShare.None))
        {
            using (ZipOutputStream stream2 = new ZipOutputStream(stream))
            {
                foreach (string str in pathnames)
                {
                    FileStream stream3 = new FileStream(str, FileMode.Open, FileAccess.Read, FileShare.Read);
                    byte[] buffer = new byte[stream3.Length];
                    try
                    {
                        if (stream3.Read(buffer, 0, buffer.Length) != buffer.Length)
                        {
                            throw new Exception(string.Format("Error reading '{0}'.", str));
                        }
                    }
                    finally
                    {
                        stream3.Close();
                    }
                    ZipEntry entry = new ZipEntry(Path.GetFileName(str));
                    stream2.PutNextEntry(entry);
                    stream2.Write(buffer, 0, buffer.Length);
                }
                stream2.Finish();
            }
        }
    }
    catch (Exception)
    {
        File.Delete(zipPathname);
        throw;
    }
}
2个回答

5
你正在尝试创建和文件大小一样大的缓冲区。相反,应将缓冲区设为固定大小,读取其中一些字节,并将读取的字节数写入zip文件中。
这是一个具有4096字节缓冲区(以及一些清理代码)的示例:
public static void CompressFiles(List<string> pathnames, string zipPathname)
{
    const int BufferSize = 4096;
    byte[] buffer = new byte[BufferSize];

    try
    {
        using (FileStream stream = new FileStream(zipPathname,
            FileMode.Create, FileAccess.Write, FileShare.None))
        using (ZipOutputStream stream2 = new ZipOutputStream(stream))
        {
            foreach (string str in pathnames)
            {
                using (FileStream stream3 = new FileStream(str,
                    FileMode.Open, FileAccess.Read, FileShare.Read))
                {
                    ZipEntry entry = new ZipEntry(Path.GetFileName(str));
                    stream2.PutNextEntry(entry);

                    int read;
                    while ((read = stream3.Read(buffer, 0, buffer.Length)) > 0)
                    {
                        stream2.Write(buffer, 0, read);
                    }
                }
            }
            stream2.Finish();
        }
    }
    catch (Exception)
    {
        File.Delete(zipPathname);
        throw;
    }
}

特别注意以下代码块:

const int BufferSize = 4096;
byte[] buffer = new byte[BufferSize];
// ...
int read;
while ((read = stream3.Read(buffer, 0, buffer.Length)) > 0)
{
    stream2.Write(buffer, 0, read);
}

这段代码读取字节并将其写入buffer中。当没有更多的字节时,Read()方法返回0,那么我们就停止。当Read()成功时,我们可以确定缓冲区中有一些数据,但我们不知道有多少字节。整个缓冲区可能被填满,也可能只有一小部分。因此,我们使用读取的字节数read来确定需要写入ZipOutputStream的字节数。
顺便说一下,这一段代码可以被一个简单的语句替换,该语句在.Net 4.0中添加,完全做到了相同的效果。
stream3.CopyTo(stream2);

所以,你的代码可以变成:

public static void CompressFiles(List<string> pathnames, string zipPathname)
{
    try
    {
        using (FileStream stream = new FileStream(zipPathname,
            FileMode.Create, FileAccess.Write, FileShare.None))
        using (ZipOutputStream stream2 = new ZipOutputStream(stream))
        {
            foreach (string str in pathnames)
            {
                using (FileStream stream3 = new FileStream(str,
                    FileMode.Open, FileAccess.Read, FileShare.Read))
                {
                    ZipEntry entry = new ZipEntry(Path.GetFileName(str));
                    stream2.PutNextEntry(entry);

                    stream3.CopyTo(stream2);
                }
            }
            stream2.Finish();
        }
    }
    catch (Exception)
    {
        File.Delete(zipPathname);
        throw;
    }
}

现在你知道为什么会出现错误,并且知道如何使用缓冲区。


谢谢。我在使用.NET 2.0上遇到了困难,这就是为什么您的包含两者的答案已经被接受的原因。 - CathalMF

4
你没有必要为此分配如此多的内存,我猜你在使用32位进程。32位进程在正常情况下只能分配最多2GB的虚拟内存,而这个库肯定也会分配内存。
无论如何,这里有几个问题:
- byte[] buffer = new byte[stream3.Length]; 为什么?你不需要把整个东西都存储在内存中才能处理它。
- 如果(stream3.Read(buffer, 0, buffer.Length) != buffer.Length) 这是一个麻烦的问题。Stream.Read明确允许返回少于你请求的字节,这仍然是有效的结果。当将流读入缓冲区时,必须重复调用Read,直到填满缓冲区或达到流的末尾。
- 你的变量应该有更有意义的名称。你很容易迷失在这些stream2、stream3等名字中。
一个简单的解决方案是:
using (var zipFileStream = new FileStream(zipPathname, FileMode.Create, FileAccess.Write, FileShare.None))
using (ZipOutputStream zipStream = new ZipOutputStream(zipFileStream))
{
    foreach (string str in pathnames)
    {
        using(var itemStream = new FileStream(str, FileMode.Open, FileAccess.Read, FileShare.Read))
        {
            var entry = new ZipEntry(Path.GetFileName(str));
            zipStream.PutNextEntry(entry);
            itemStream.CopyTo(zipStream);
        }
    }
    zipStream.Finish();
}

2
Stream.CopyTo比我的do..while循环更易读,点个赞。 - CodeCaster
谢谢。我会记下这个,以备将来的项目使用,但是对于这个项目,我被困在了 .Net 2.0 上。我应该在问题中提到这一点。 - CathalMF
当然,在这种情况下,while循环是适当的,但您也可以使用SharpZipLib的StreamUtils.Copy函数。 - Lucas Trzesniewski

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