StringBuilder出现有趣的OutOfMemoryException

14

我需要在循环中持续构建大字符串并将其保存到数据库,但当前偶尔会出现OutOfMemoryException

基本上是这样的:我使用XmlWriterStringBuilder根据某些数据创建了一个字符串。然后我调用外部库中的方法将该xml字符串转换为其他字符串。之后,转换后的字符串被保存到数据库中。对于不同的数据,这整个过程都要在循环中重复约100次。

这些字符串本身并不是太大(每个都不到500k字节),而且在此循环期间进程内存也没有增加。但是,偶尔我会在StringBuilder.Append中遇到OutOfMemeoryExcpetion异常。有趣的是,这种异常不会导致崩溃。我可以捕获这个异常并继续执行循环。

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

考虑到我不能避免转换所有这些字符串,我应该怎么做才能使它可靠地工作?我应该强制进行GC回收吗?我应该在循环中放一个Thread.Sleep吗?我应该停止使用StringBuilder吗?当遇到OutOfMemoryException时,应该简单地重试吗?

3个回答

17

您的字符串生成器有内存空间但没有足够大小的连续段。您需要知道,每当字符串生成器的缓冲区太短时,它的大小会加倍。如果您可以在构造函数中定义生成器的大小,那么最好。

当您完成大量对象的收集时,可以调用GC.Collect()

实际上,在出现Out Of Memory(内存不足)时,通常表示设计不良,您可以使用硬盘(临时文件)而非内存,不应该一遍又一遍地分配内存(尝试重用对象/缓冲区等)。

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


如果我一遍又一遍地使用同一个 StringBuilder,那么我会使用相同的内存段(除非 SB 需要扩大自身),这样不是可以解决我的所有问题吗(很可能)? - bitbonk
我不确定,你需要尝试一下看看字符串构建器在清空时是否保留其容量... - Guillaume
不是完全无意义的:你可以避免内存碎片化,这似乎是真正的问题所在。 - Guillaume
如果垃圾中没有其他SB,那么它可以重新分配,这将更加高效。 - Guillaume
一个字符串构建器有一个缓冲区,如果您重复使用该构建器,则可以重复使用该缓冲区。 如果每次创建新的字符串构建器,则每次都会创建新的缓冲区,旧的缓冲区将成为垃圾。 即使重新使用了大小调整的字符串构建器,由于仅存在来自此构建器而不是来自先前操作的所有字符串构建器的缓冲区,因此垃圾量也少得多。 - Guillaume

3

在生成数据时,尽量重复使用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

根据你提到的大小,你可能遇到了大对象堆(LOH)碎片问题。

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

如果将分配舍入到20k的倍数左右,可能有所帮助。这可以改善重用情况。


为什么重用 StringBuilder 对象不是一个直接的解决方案?你的意思是仅仅重用并且预先分配(一个大小适合所有未来字符串的)空间就可以解决问题吗? - bitbonk
bitbonk,是的,类似这样。LOH最糟糕的模式是按递增大小分配块和/或交替短寿命和长寿命块。 - H H

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