收回垃圾(GC.Collect)是否有必要?

24
阅读这篇旧但经典的文档编写高性能托管应用程序 - 入门指南,我看到以下声明:
引用:

GC是自我调整的,根据应用程序的内存要求进行调整。在大多数情况下,通过编程方式调用GC会妨碍该调整。“帮助”GC通过调用GC.Collect很可能不会提高应用程序的性能。

我正在处理在某一时间点消耗大量内存的应用程序。当我完成代码中消耗该内存的操作时,我调用GC.Collect。如果我不这样做,我会得到内存不足异常。这种行为不一致,但大约30%的时间,我会遇到内存不足异常。添加了GC.Collect后,我从未遇到过此内存不足异常。即使这份最佳实践文件反对我的做法,我的行动也是合理的吗?
4个回答

30

GC的运行机制中,内存中的对象是“分代”的,早期代被更频繁地垃圾回收,这有助于通过不一直回收长期存在的对象来提高性能。

因此,当您自己调用GC.Collect()时会发生两件事情。第一,您会花费更多时间进行垃圾回收,因为正常的后台回收将继续进行,而您的手动GC.Collect()也要同时执行。第二,在某些情况下,您会将内存占用时间延长,因为您强制将某些东西放入高级别代中,而它们实际上不需要进入那里。换句话说,自己使用GC.Collect()几乎总是一个坏主意。

在一些情况下,垃圾回收器的性能并不一定好。其中之一是大对象堆,这是专门为大于某个大小(80,000字节,如果我没记错的话)的对象创建的特殊代。这一代很少回收,几乎从不压缩。这意味着随着时间的推移,您可以在内存中留下许多可观的空洞,但这些空间实际上没有被占用,是可以被其他进程使用的。但它仍然在您的进程中占用了地址空间,而默认情况下您的地址空间限制为2GB。

这是OutOfMemory异常的一个非常常见的来源,因为您并没有实际使用那么多内存,但是您所有的地址空间都被大对象堆的空洞占据。这种情况最常见的发生方式是反复追加大字符串或文档。也许这不是您的情况,因为在这种情况下,无论调用多少次GC.Collect()均无法压缩LOH,但是在您的情况下,似乎有所帮助。然而,这是我见过的大多数OutOfMemory异常的根源。

垃圾回收器不总是能够有效地工作的另一个场景是某些事物导致对象保持根引用。 一个例子是事件处理程序可能会阻止对象被回收。 解决这个问题的方法是确保每个 += 操作都有相应的 -= 操作来取消订阅它。 但是,GC.Collect() 在这里不太可能有帮助 - 对象仍然在某个地方保持着引用,因此无法被回收。

希望这为您提供了解决首先需要使用GC.Collect()的根本问题的调查途径。 但如果没有,当然最好有一个可运行的程序而不是一个失败的程序。 在任何我使用GC.Collect()的地方,我都会确保代码有良好的文档记录,解释为什么需要它(否则会出现异常),以及重现它所需的确切步骤和数据,以便未来的程序员可以确定在何时安全地删除它。


2
给非常有见地的想法点个赞。 - palm snow
3
实际上调用GC.Collect()可能并没有想象中那么糟糕(就像我最初想的那样),特别是阅读了Jeffrey Richter在这篇文章中的观点http://msdn.microsoft.com/en-us/magazine/bb985011.aspx "由于您的应用程序比运行时更了解其行为,因此通过显式地强制一些收集,您可以帮助解决问题。"尽管我仍然好奇为什么GC.Collect会帮助解决LOH上对象导致OOM的问题,因为GC.Collect对此没有控制权。 - palm snow
@palmsnow,我也很好奇。经过大量搜索,我在这里找到了答案:https://dev59.com/amkw5IYBdhLWcg3wQ4Zc - SiberianGuy
1
@palmsnow 在单线程应用程序中这是一个还算不错的论点,但如果你进行任何重要的异步或多线程代码,它就会完全崩溃。我甚至都记不得上一次我在真正的单线程应用程序上工作了。请记住,GC.Collect 会影响(并冻结)整个进程。 - Luaan

7
大多数人会认为使您的代码正常工作比使其快速更重要。因此,如果在不调用GC.Collect()时,它在30%的时间内无法正常工作,则这将优先考虑其他所有问题。
当然,这引出了一个更深层次的问题:“为什么会出现OOM错误?是否存在应该修复而不仅仅是调用GC.Collect()的更深层次问题。”
但是,您找到的建议涉及性能。如果性能使您的应用程序有30%的失败率,您会关心性能吗?

@jalf,这个应用程序基本上是将一张图片与存储在内存中的图像库进行比较。根据图像的大小和缓存大小(由应用程序用户配置),我们可能会遇到导致OOM(内存溢出)的情况。在那个时候,我显然更需要应用程序的可用性而不是性能(这就是为什么我添加了GC.Collect()),但是我也在努力确定可伸缩性、性能和可用性之间的平衡点。 - palm snow
@palm - 我敢打赌你的图像都存储在大对象堆中。如果你加载和卸载了大量图像,随着时间的推移,大对象堆将会出现碎片化。如果你可以使用小于80000字节的分段来进行比较,那么大对象堆就不会参与其中,你的问题也将消失。根据你的比较方式,这很可能也会更快,因为这意味着你不需要每次对库中的每个图像都评估整个图像。 - Joel Coehoorn
1
@palm snow:你是在对图像进行模糊比较还是精确的逐像素匹配?如果你正在进行精确匹配,那么你应该考虑生成/缓存/匹配哈希值而不是位图本身——这样会更快,也更节省内存。 - LukeH
@palm snow:GC仍然会在LOH中"收集"对象(每当进行gen-2收集时,如果我没记错的话)。GC不会"压缩"LOH;但它会标记已释放的段,以便重新使用。 - LukeH
@LukeH 从 LOH 回收对象并不会压缩它,因此大小仍然保持不变。所以我仍然困惑为什么 GC.Collect 将有助于处理此情况下的 OOM。 - palm snow
显示剩余2条评论

2

一般来说,不应该需要使用GC.Collect。如果您的图像存在于非托管内存中,请确保适当使用GC.AddMemoryPressureGC.RemoveMemoryPressure


0
从您的描述中,听起来像是没有处理Disposeable对象,或者在操作之前没有设置将被替换为null的成员值。以下是后者的示例:
  • 获取表格,在网格中显示
  • (用户点击刷新)
  • 数据刷新时禁用表单
  • 查询返回,新数据填充到网格中
您可以在此期间清除网格,因为它即将被替换;如果不这样做,您将在替换时暂时拥有两个表格(不必要地)存储在内存中。

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