【问题标题】:To GC.Collect or not?GC.Collect 与否?
【发布时间】:2011-03-09 16:27:36
【问题描述】:

阅读这个古老但经典的文档Writing High-Performance Managed Applications - A Primer,我遇到了以下声明

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

我正在使用在给定时间点消耗大量内存的应用程序。当我完成消耗该内存的代码时,我正在调用 GC.Collect。如果我不这样做,我会得到内存不足异常。这种行为不一致,但粗略地说,有 30% 的时间,我会失忆。添加 GC.Collect 后,我​​从未遇到过内存不足异常。即使此最佳实践文档反对我的行为,我的行为是否合理?

【问题讨论】:

    标签: .net garbage-collection clr


    【解决方案1】:

    GC 中发生的部分原因是内存中的对象是分代的,因此早期代的收集比其他代更频繁。这有助于通过不尝试一直收集长寿命对象来节省性能。

    考虑到这一点,当您自己拨打GC.Collect() 时,可能会发生两件事。首先是您最终会花费更多时间来收集。这是因为除了您的手动 GC.Collect() 之外,仍然会发生正常的后台收集。第二个是你会在内存更长上挂着,因为你强迫一些东西进入一个不需要去那里的更高阶的生成。换句话说,自己使用 GC.Collect() 几乎总是一个坏主意。

    在某些情况下,垃圾收集器并不总是表现良好。其中之一是大型对象堆。这是大于特定大小(80,000 字节,IIRC,但现在可能已经过时)的对象的特殊生成。这一代人几乎从未被收集过,也几乎从未压缩。这意味着随着时间的推移,您最终可能会在内存中出现许多无法释放的相当大的漏洞。物理内存并未实际使用,可供其他进程使用,但它仍会消耗您进程内的地址空间,默认情况下您限制为 2GB。

    这是 OutOfMemory 异常的一个非常常见的来源——您实际上并没有使用那么多内存,但是您的所有这些地址空间都被大对象堆中的空洞占用了。到目前为止,发生这种情况的最常见方式是重复附加到大字符串或文档。这可能不是您,因为在这种情况下,对 GC.Collect() 的任何调用都不会压缩 LOH,但在您的情况下,它似乎确实有帮助。然而,这是我见过的绝大多数 OutOfMemory 异常的来源。

    垃圾收集器不总是表现良好的另一个地方是当某些事情导致对象保持根时。一个例子是事件处理程序可以防止对象被收集。解决这个问题的方法是确保每个订阅事件的+= 操作都有一个对应的-= 操作来取消订阅它。但同样,GC.Collect() 在这里不太可能有帮助 - 对象仍然植根于某处,因此无法收集。

    希望这为您提供了一种调查途径,以解决您首先需要使用 GC.Collect() 的潜在问题。但如果不是这样,当然,拥有一个工作程序总比一个失败的程序好。在我使用 GC.Collect() 的任何地方,我都会确保代码有详细的文档说明,说明您需要它的原因(您会得到例外)以及可靠地重现它所需的确切步骤和数据以便将来可能想要删除它的程序员可以确定何时可以安全地这样做。

    【讨论】:

    • +ve 为非常有见地的想法投票
    • 实际上调用 GC.Collect() 可能并不算太糟糕(正如我最初认为的那样),尤其是阅读 Jeffery Richter msdn.microsoft.com/en-us/magazine/bb985011.aspx 的这篇文章“因为你的应用程序比运行时更了解它的行为,您可以通过明确强制一些集合来帮助解决问题”。虽然我仍然很好奇为什么 GC.Collect 在对象位于 LOH 时帮助解决 OOM,而 GC.Collect 无法控制。
    • @palmsnow,我也很好奇。经过大量搜索,我在这里找到了答案:stackoverflow.com/questions/10016541/…
    • @palmsnow 这在单线程应用程序中是一个不错的论点,但如果您执行任何重要的异步或多线程代码,它就会完全崩溃。我什至不记得我上一次开发真正的单线程应用程序是什么时候了。请记住,GC.Collect 会影响(并冻结)整个过程。
    【解决方案2】:

    大多数人会说,让您的代码正确运行比让它快速运行更重要。因此,当您不致电 GC.Collect() 时,它在 30% 的情况下无法正常工作,这胜过所有其他问题。

    当然,这会导致更深层次的问题:“为什么会出现 OOM 错误?是否应该解决更深层次的问题,而不是仅仅调用 GC.Collect()

    但是您找到的建议是关于性能的。如果它让您的应用在 30% 的时间内失败,您是否关心性能?

    【讨论】:

    • @jalf,此应用程序基本上将一张图像与缓存到内存中的图像库进行比较。根据图像大小和缓存大小(由应用程序用户配置),我们可能会遇到导致 OOM 的情况。在这一点上,我显然需要应用程序可用性而不是性能(这就是我添加 GC.Collect() 的原因),但是试图确定可扩展性和性能与可用性之间的细线。
    • @palm - 我敢打赌,你的图像在大对象堆上。您加载和卸载一堆图像,随着时间的推移,LOH 会碎片化。如果您可以使用小于 80000 字节的段以块进行比较,则永远不会涉及 LOH,您的问题就会消失。根据您的比较,这也可能快得多,因为这可能意味着您不需要每次都为库中的每个图像评估整个图像。
    • @Joel,你是对的;每张图片的大小至少为 1MB,我们可以在缓存中保存超过 50,000 张图片。
    • @palm snow:您是在对图像进行某种模糊比较还是逐像素精确匹配?如果您正在执行完全匹配,那么您应该考虑生成/缓存/匹配哈希而不是位图本身 - 它会更快并且内存消耗更少。
    • @LukeH:遗漏了一个小细节:) 我们正在使用第三方库来进行比较。不确定他们的逻辑。但是鉴于这些图像位于 LOH 上,调用 GC.Collect() 将无助于解决这个碎片问题。关于为什么添加对 GC.Collect() 的调用可以使应用程序在这种情况下稳定的任何想法?
    【解决方案3】:

    一般来说,GC.Collect 不是必需的。如果您的图像存在于非托管内存中,请务必正确使用GC.AddMemoryPressureGC.RemoveMemoryPressure

    【讨论】:

      【解决方案4】:

      从您的描述看来,Disposeable 对象没有被释放,或者您没有设置在操作之前将被替换为 null 的成员值。以后者为例:

      • 获取表格,在网格中显示
      • (用户点击刷新)
      • 刷新数据时禁用表单
      • 查询返回,新数据填充到网格中

      您可以在此期间清除网格,因为无论如何它都将被替换;如果您不这样做,您将暂时将两个表都保存在内存中(不必要地)。

      【讨论】:

        猜你喜欢
        • 2012-05-11
        • 2012-12-06
        • 2019-10-25
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-07-06
        • 2022-01-10
        相关资源
        最近更新 更多