如何在C#中释放或回收字符串?

11

我有一个很长的字符串(例如20MB)。

我现在正在解析这个字符串。问题是C#中的字符串是不可变的,这意味着一旦我创建了一个子字符串并查看它,内存就会被浪费。

由于所有的处理过程,内存被String对象占用了,而且我不再使用、需要或者引用它们;但垃圾回收器需要很长时间才能释放它们。

所以应用程序会因为内存不足而运行失败。

我可以使用效率低下的“合并”方法,并分散几千次调用:

GC.Collect();

虽然字符串无处不在,但这并没有真正解决问题。

我知道使用 StringBuilder 可以在创建大型字符串时使用。

我知道 TextReader 可以将一个 String 读入到一个 char 数组中。

我需要以某种方式“重用”一个字符串,使其不再是不可变的,从而在只需要1k的情况下避免不必要地分配数GB内存。


我猜你可能还有一些你没有意识到的引用。 - David Heffernan
3
如果你正在创建一个一千万字符长的字符串,很可能你做错了什么。为什么首先在内存中需要这么大的字符串?你需要将整个字符串放在内存中才能解析它吗?解析器通常以有限的前向方式消耗字符串,有限的先行查看;为什么你需要一次性在内存中存储整个字符串? - Eric Lippert
@Eric Lippert:我们正在处理MHT文件;每个文件都作为内存中的字符串。MHT是一个单文件网页;客户将其用作人员的可移植表示。它包含足够大的经过Base64编码的图像以进行面部识别。在数据库位于硬盘或CD上的情况下,我们可以使用StreamReader(并且在从网站中获取它们时使用StringReader)-但是然后我们必须将其处理为Char数组。这样做会使我们失去所有有用的方法String(StartsWith、SubString、IndexOf)带给我们的好处。而且它已经被写入了。 - Ian Boyd
一个字符串不是“持久”的数据结构;也就是说,当它被改变时,它并没有被设计成可以有效地重复使用内存。因此您几乎肯定会遇到各种问题。(请参见http://blogs.msdn.com/b/ericlippert/archive/2011/07/19/strings-immutability-and-persistence.aspx了解更多分析。)如果我是你,我会构建一个比字符串更好的抽象;我会使用基于流的方法,将字符流转换为标记流,然后将标记流转换为解析节点流。 - Eric Lippert
相反,我会考虑构建一个结构体,有效地表示现有字符串的子字符串,而不是实际构建子字符串。也就是说,创建自己的持久数据结构,该结构层叠在现有字符串之上,而不是在执行某些非持久操作时制作字符串的大副本。这些是我们在编译器中构建的抽象,用于处理需要解析的多兆字节源代码文件等问题。 - Eric Lippert
显示剩余3条评论
4个回答

12

如果您的应用程序崩溃了,很可能是因为您仍然有对字符串的引用,而不是垃圾收集器无法清理它们。我确实见过它失败,但这种情况相当罕见。您是否使用分析器检查了您同时在内存中拥有大量字符串的情况?

长话短说,您不能重复使用一个字符串来存储不同的数据 - 这是不可能的。如果您喜欢,可以编写自己的等效代码 - 但有效且正确地完成这件事的机会非常小。现在,如果您能提供更多关于您正在做什么的信息,我们可能能够建议使用不那么占用内存的替代方法。


很可能Jon是正确的,你可能持有一些与字符串相关的引用,从而阻止了清理。然而,如果不是这种情况并且您必须重用字符串的内存,您可以考虑使用'不安全'代码,但这只能作为最后的手段。您可以在此处找到更多详细信息http://msdn.microsoft.com/en-us/library/aa288474%28v=vs.71%29.aspx。 - Jeff Reddy
我们并没有保留一个引用,如果我们强制垃圾回收运行,内存就会被释放。在某种程度上,你可以认为我们持有这个内存,因为它确实是在我的进程空间中分配的。除了垃圾回收器运行得不够快之外,没有什么能阻止它释放内存。 - Ian Boyd
@Ian:这至少是不寻常的。子字符串有多大?原始大字符串的来源是什么,是否可以流式传输(例如逐行处理)? - Jon Skeet
@Jon:我们可以流式传输它们,但必须是其他类型而不是Strings,因为我们会回到原点。在我看来,处理它的唯一方法是要么.Read进入Char数组(并编写自己的Char[]字符串库),要么定期调用GC.Collect()。前者的缺点是需要完全重写(逻辑上)正确的代码,并且必须创建自己的(几乎肯定有错误的)字符串库。后者的缺点是感觉像个hack。 - Ian Boyd
大对象堆中的碎片化空闲空间症状看起来与我们的情况完全匹配。如果可能的话,将对象限制在较小的对象堆中有望帮助垃圾回收器跟得上。 - Ian Boyd
显示剩余9条评论

3
这个问题已经快有10年了。如今,请查看ReadOnlySpan - 使用AsSpan()方法从字符串中实例化一个只读的ReadOnlySpan。然后,您可以应用索引运算符来获取切片,而不需要分配任何新的字符串。

2
我建议考虑事实,即在C#中无法重用字符串,使用内存映射文件。您可以将字符串保存在磁盘上,并通过映射文件像流一样处理它,从而实现性能/内存消耗卓越的关系。在这种情况下,您可以重复使用相同的文件、相同的流,并且仅在需要的那个特定时刻操作数据的小部分,例如字符串,然后立即将其丢弃。
这种解决方案严格取决于您的项目要求,但我认为这是您可以认真考虑的解决方案之一,尤其是内存消耗将大大降低,但在性能方面会“付出”一些代价。

即使文件被内存映射,我们仍然存在一个问题,即我们将部分内容读入到“字符串”中 - 有时是大部分,有时是小部分,有时是从较大部分中提取的较小部分。最终,所有这些未收集的字符串会耗尽可用内存 - 或导致交换死机。 - Ian Boyd
@Ian 频繁但短期使用相对较小的字符串,因为在这种情况下您不需要将所有数据加载到内存中,所以应该会有显着的差异。 - Tigran

1
你有一些示例代码可以测试可能的解决方案是否有效吗?
总的来说,任何大于85KB的对象都将被分配到大对象堆上,这可能会更少地进行垃圾回收。
此外,如果你真的在努力推动CPU,垃圾回收器可能会更少地执行其工作,试图避免干扰你。

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