使用SharpZipLib压缩大文件导致Out Of Memory异常

时间:2015-01-30 12:18:46

标签: c# compression out-of-memory sharpziplib

我有一个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 个答案:

答案 0 :(得分:4)

你没有充分的理由分配大量内存,我打赌你有32位进程。在正常情况下,32位进程最多只能分配2GB的虚拟内存,而且库肯定也会分配内存。

无论如何,这里有几件事是错的:

  • byte[] buffer = new byte[stream3.Length];

    为什么呢?你不需要将整个东西存储在内存中来处理它。

  • if (stream3.Read(buffer, 0, buffer.Length) != buffer.Length)

    这个很讨厌。明确允许Stream.Read返回 less 字节而不是您要求的字节,这仍然是有效的结果。将流读入缓冲区时,必须重复调用Read,直到填充缓冲区或到达流的末尾。

  • 您的变量应该有更有意义的名称。使用这些stream2stream3

  • ,您很容易迷失方向

一个简单的解决方案是:

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

答案 1 :(得分:2)

您正在尝试创建与文件一样大的缓冲区。相反,将缓冲区设置为固定大小,将一些字节读入其中,并将读取的字节数写入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;
    }
}

现在您知道为什么会出现错误,以及如何使用缓冲区。