【问题标题】:Interesting OutOfMemoryException with StringBuilder有趣的 OutOfMemoryException 与 StringBuilder
【发布时间】:2010-12-18 16:09:48
【问题描述】:

我需要在循环中不断构建大字符串并将它们保存到目前偶尔会产生OutOfMemoryException的数据库中。

这里的基本情况是我根据一些数据使用XmlWriterStringBuilder 创建一个字符串。然后我从外部库中调用一个方法,将这个 xml 字符串转换为其他字符串。之后,转换后的字符串将保存到数据库中。整个过程针对不同的数据循环重复执行大约 100 次。

字符串本身并不太大(每个小于 500kByte),并且在此循环期间进程内存没有增加。但是,偶尔我会在StringBuilder.Append 中得到OutOfMemeoryExcpetion。有趣的是,这个异常不会导致崩溃。我可以捕获该异常并继续循环。

这里发生了什么?为什么我会得到OutOfMemoryException,尽管系统中仍有足够的可用内存?这是一些 GC 堆问题吗?

鉴于我无法绕过转换所有这些字符串,我可以做些什么来使这项工作可靠地工作?我应该强制进行 GC 收集吗?应该将Thread.Sleep 放入循环中吗?我应该停止使用StringBuilder 吗?遇到OutOfMemoryException 时是否应该简单地重试?

【问题讨论】:

    标签: c# memory garbage-collection stringbuilder


    【解决方案1】:

    有内存,但没有可以处理字符串生成器大小的连续段。您必须知道,每次字符串生成器的缓冲区太短时,其大小都会增加一倍。如果您可以(在 ctor 中)定义构建器的大小,那就更好了。 处理完大量对象后,您可以致电 GC.Collect()

    实际上,当你有一个 OutOfMemory 时,它通常显示一个糟糕的设计,你可以使用硬盘驱动器(临时文件)而不是内存,你不应该一次又一次地分配内存(尝试重用对象/缓冲区/. ..)。

    我强烈建议您阅读 Eric Lippert 的这篇 “Out Of Memory” Does Not Refer to Physical Memory 帖子。

    【讨论】:

    • 如果我一遍又一遍地使用相同的 StringBuilder,我是否会使用相同的内存段(除非 SB 需要扩大自身)?这不会解决我所有的问题(很可能)吗?
    • 我不确定,您必须尝试查看字符串生成器在清除时是否保持其容量...
    • 并非完全没有意义:您可以避免内存碎片,这似乎是这里真正的问题。
    • 好吧,它可以重新分配,但如果垃圾中没有其他SB,它会效率更高。
    • 一个SB有一个缓冲区,如果你重用SB,你可以重用缓冲区。如果每次都创建一个新的 SB,那么每次都会创建一个新的缓冲区,而旧的缓冲区则处于垃圾中。即使重用的字符串构建器被放大了,垃圾也少了很多,因为只有来自这个构建器的缓冲区,而不是来自先前操作的所有字符串构建器的所有缓冲区。
    【解决方案2】:

    在生成数据时尝试重用 StringBuilder 对象。

    在使用之后或之前,只需将 StringBuilder 的大小重置为 0 并开始追加。这将减少分配的数量,并可能使 OutOfMemory 情况非常罕见。

    为了说明我的观点:

    void MainProgram()
    {
        StringBuilder builder = new StringBuilder(2 * 1024); //2 Kb
    
        PerformOperation(builder);
        PerformOperation(builder);
        PerformOperation(builder);
        PerformOperation(builder);
    }
    
    void PerformOperation(StringBuilder builder)
    {
        builder.Length = 0;
    
        //
        // do the work here builder.Append(...);
        //
    }
    

    【讨论】:

      【解决方案3】:

      对于您提到的大小,您可能会遇到Large Object Heap (LOH) 碎片。

      重用 StringBuilder 对象不是直接的解决方案,您需要掌握底层缓冲区。
      如果可能,请事先计算或估计大小并预先分配。

      如果您将分配向上四舍五入(假设是 20k 左右的倍数),它可能会有所帮助。这可以提高重用性。

      【讨论】:

      • 为什么重用 StringBuilder 对象不是一个直接的解决方案?您的意思是重用 + 初始预分配(大小适合所有未来字符串)将是解决方案吗?
      • bitbonk,是的,类似的东西。 LOH 最糟糕的模式是分配越来越大的块和/或交替使用短期和长期存在的块。
      猜你喜欢
      • 2011-11-24
      • 2012-11-15
      • 2023-02-22
      • 2012-10-25
      • 2017-05-27
      • 2020-06-01
      • 1970-01-01
      • 1970-01-01
      • 2011-01-26
      相关资源
      最近更新 更多