【问题标题】:How to free up memory after base64 convertbase64转换后如何释放内存
【发布时间】:2011-03-25 17:36:02
【问题描述】:

我正在尝试流式传输文件的内容。 该代码适用于较小的文件,但对于较大的文件,我会收到 Out of Memory 错误。

public void StreamEncode(FileStream inputStream, TextWriter tw)
{
    byte[] base64Block = new byte[BLOCK_SIZE];
    int bytesRead = 0;

    try
    {
        do
        {
            // read one block from the input stream
            bytesRead = inputStream.Read(base64Block, 0, base64Block.Length);

            // encode the base64 string
            string base64String = Convert.ToBase64String(base64Block, 0, bytesRead);

            // write the string
            tw.Write(base64String);

        } while (bytesRead == base64Block.Length);
    }
    catch (OutOfMemoryException)
    {
        MessageBox.Show("Error -- Memory used: " + GC.GetTotalMemory(false) + " bytes");
    }
}

我可以隔离问题并观察使用的内存在循环过程中增长。
问题似乎是对Convert.ToBase64String() 的调用。

如何为转换后的字符串释放内存?


从这里开始编辑......这是一个更新。 我还为此创建了一个新的 thread —— 抱歉,我想这不是正确的做法。

感谢您的精彩建议。根据建议,我缩小了用于从文件读取的缓冲区大小,看起来内存消耗更好,但我仍然看到 OOM 问题,而且我看到文件大小小至 5MB 时出现此问题。我可能想处理十倍大的文件。

我现在的问题似乎在于使用 TextWriter。

我创建一个请求如下 [通过一些编辑来缩小代码]:

HttpWebRequest oRequest = (HttpWebRequest)WebRequest.Create(new Uri(strURL));
oRequest.Method = httpMethod;
oRequest.ContentType = "application/atom+xml";
oRequest.Headers["Authorization"] = getAuthHeader();
oRequest.ContentLength = strHead.Length + strTail.Length + longContentSize;
oRequest.SendChunked = true;

using (TextWriter tw = new StreamWriter(oRequest.GetRequestStream()))
{
    tw.Write(strHead);
    using (FileStream fileStream = new FileStream(strPath, FileMode.Open, 
           FileAccess.Read, System.IO.FileShare.ReadWrite))
    {
        StreamEncode(fileStream, tw);
    }
    tw.Write(strTail);
}
.....

调用例程:

public void StreamEncode(FileStream inputStream, TextWriter tw)
{
    // For Base64 there are 4 bytes output for every 3 bytes of input
    byte[] base64Block = new byte[9000];
    int bytesRead = 0;
    string base64String = null;

    do
    {
        // read one block from the input stream
        bytesRead = inputStream.Read(base64Block, 0, base64Block.Length);

        // encode the base64 string
        base64String = Convert.ToBase64String(base64Block, 0, bytesRead);

        // write the string
        tw.Write(base64String);


    } while (bytesRead !=0 );

}

由于潜在的大内容,我应该使用 TextWriter 以外的东西吗?能够创建请求的整个有效负载似乎非常方便。

这完全是错误的方法吗?我希望能够支持非常大的文件。

【问题讨论】:

  • 你不应该捕捉 OutOfMemoryException (事实上,在 .NET4 中,你不能,至少不诉诸 syntactic salt )。现在,关于这个问题...... BLOCK_SIZE 是什么,您使用的是什么 TextWriter,以及读取了多少字节?其中一项或多项可能是罪魁祸首。
  • 我添加了 try/catch 来帮助诊断这个问题。这不是我最初写的方式。
  • BLOCK_SIZE 的值为 54000。我将其减小到 30000,但内存仍在增长——但现在需要更多循环,因为它更小了。
  • 块大小可能是 32kB 或更大,在 LOH 中产生了太多的大字符串。较小的尺寸很好,Convert.ToBase64CharArray() 最好。

标签: c# memory-leaks out-of-memory


【解决方案1】:

如果您使用 32 kB 或更大的 BLOCK_SIZE,您将创建 85 kB 或更大的字符串,这些字符串分配在大对象堆上。短期对象应该存在于常规堆中,而不是大型对象堆中,因此这可能是内存问题的原因。

另外,我发现代码有两个潜在问题:

  • base64 编码在字符串末尾使用填充,因此,如果您将流切分成位并转换为 base64 字符串,然后将字符串写入流,您最终不会得到单个 base64 流。

  • 检查使用Read 方法读取的字节数是否与请求的字节数相同不是检查流结尾的正确方法。 Read 方法在任何时候都可能读取比请求更少的字节数,而检查流结束的正确方法是在方法返回零时。

【讨论】:

  • 要扩展您指出的两个附加问题:OP 将需要添加逻辑以确保仅将大小为 3 的倍数的输入传递给 Convert.ToBase64String 方法。最后一个块可以是任意大小。
  • 一般来说这些都是很棒的建议,但在这种情况下它本身不应导致 OOM,因为此代码中的这些块将由于内存分配请求而被收集。
  • @Alexei Levenkov:这适用于常规堆,但大对象堆的管理方式不同。它可以通过频繁的分配和释放进行碎片整理,并且永远不会像常规的堆生成那样被完全清除。
  • 在降低 BLOCK_SIZE 和在请求对象上设置 AllowWriteStreamBuffering 标志的组合之间,内存问题对我来说已经消失了。谢谢大家!
【解决方案2】:

请记住,将数据转换为 base64 时,生成的字符串将长 33%(假设输入大小是 3 的倍数,这在您的情况下可能是个好主意)。如果 BLOCK_SIZE 太大,则可能没有足够的连续内存来保存生成的 base-64 字符串。

尝试减小 BLOCK_SIZE,使 base-64 的每一块更小,从而更容易为其分配内存。

但是,如果您使用像 StringWriter 这样的内存 TextWriter,您可能会遇到同样的问题,因为它无法找到足够大的内存块来容纳内部缓冲区。不过,如果您正在写入文件之类的东西,这应该不是问题。

【讨论】:

  • 另外,由于 .NET 字符串存储为 UTF-16,base-64 字符串实际上将比原始字符串长 133%。
  • 实际上长了 166%。 :)
【解决方案3】:

猜测...HttpWebRequest.AllowWriteStreamBuffering 默认为 true,根据 MSDN,“将 AllowWriteStreamBuffering 设置为 true 可能会在上传大型数据集时导致性能问题,因为数据缓冲区可能会使用所有可用内存”。尝试设置 oRequest.AllowWriteStreamBuffering = false 看看会发生什么。

【讨论】:

  • 感谢您的猜测 PoppaVein!做到了。我在上传一个7MB的文件时检查了内存,内存在低位徘徊,然后上传成功。
【解决方案4】:

尝试将您的 base64String 声明拉出循环。如果这仍然没有帮助,请尝试在多次迭代后调用垃圾收集器。

GC.Collect(); GC.WaitForPendingFinalizers();

【讨论】:

  • 不推荐在代码中手动调用GC。请参阅blogs.msdn.com/b/ericlippert/archive/2009/06/08/… 了解更多信息。
  • 这是真的,但值得一试。当您使用 IDisposable 接口创建自定义类时,有时会在 Dispose() 方法上调用收集器。这样做我没有任何问题。
  • 为什么会这样?如果这有效,则意味着 GC 正在工作中休眠,因为系统内存不足,但 GC 并未尝试自行回收一些内存。我对此深表怀疑。
  • 我尝试将 base64String 拉出循环,但没有帮助。
  • 然后我添加了垃圾收集并看到了不同的行为。我认为它更好,但它真的没有帮助。内存在大量循环中保持不变,然后进行大跳跃。 23MB .... 27MB .... 31MB .... 40MB ... 内存不足。没有 GC,向上的变化是渐进的。
【解决方案5】:

尝试减小块大小或避免将 Convert 调用的结果分配给变量:

bytesRead = inputStream.Read(base64Block, 0, base64Block.Length);
tw.Write(Convert.ToBase64String(base64Block, 0, bytesRead));

【讨论】:

  • 赋值没有问题(不会增加内存占用),但是块大小可以。
【解决方案6】:

从内存使用的角度来看,代码看起来不错,但我认为您正在为基于内存的流(如 MemoryStream)传递写入器并将数据存储在那里会导致 OOM 异常。

如果 BLOCK_SIZE 大于 86Kb 分配将发生在大对象堆 (LOH) 上,它会改变分配行为,但本身不应导致 OOM。

注意:您的结束条件不正确 - 应该是 bytesRead != 0,一般来说,即使还有更多数据,Read 也可以返回比请求更少的字节。据我所知,FileStream 也从未这样做过。

【讨论】:

    【解决方案7】:

    我会先将结果写入临时文件。

    using (TextWriter tw = new StreamWriter(oRequest.GetRequestStream()))
    {
        tw.Write(strHead);
        var tempPath = Path.GetTempFileName();
        try
        {
            using (var input = File.OpenRead(strPath))
            using (var output = File.Open(
                tempPath, FileMode.Open, FileAccess.ReadWrite))
            {
                StreamEncode(fileStream, output);
                output.Seek(0, SeekOrigin.Begin);
                CopyTo(output, ((StreamWriter)tw).BaseStream);
            }
        }
        finally
        {
            File.Delete(tempPath);
        }
        tw.Write(strTail);
    }
    
    public void StreamEncode(Stream inputStream, Stream output)
    {
        // For Base64 there are 4 bytes output for every 3 bytes of input
        byte[] base64Block = new byte[9000];
        int bytesRead = 0;
        string base64String = null;
    
        using (var tw = new StreamWriter(output))
        {
            do
            {
                // read one block from the input stream
                bytesRead = inputStream.Read(base64Block, 0, base64Block.Length);
    
                // encode the base64 string
                base64String = Convert.ToBase64String(base64Block, 0, bytesRead);
    
                // write the string
                tw.Write(base64String);
    
            } while (bytesRead !=0 );
        }
    
    }
    
    
    static void CopyTo(Stream input, Stream output)
    {
        const int length = 10240;
        byte[] buffer = new byte[length];
        int count = 0;
    
        while ((count = input.Read(buffer, 0, length)) > 0)
            output.Write(buffer, 0, count);
    }
    

    【讨论】:

    • 感谢您花时间写这篇文章。由于 AllowWriteStreamBuffering 标志在请求中为我工作,我想我现在不需要它。
    猜你喜欢
    • 2017-02-01
    • 2015-01-31
    • 2015-10-10
    • 2016-10-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-01-31
    相关资源
    最近更新 更多