【问题标题】:Is correct to use GC.Collect(); GC.WaitForPendingFinalizers();?使用 GC.Collect() 是正确的; GC.WaitForPendingFinalizers();?
【发布时间】:2012-09-04 14:26:18
【问题描述】:

我开始在一个项目中查看一些代码,发现是这样的:

GC.Collect();
GC.WaitForPendingFinalizers();

这些行通常出现在旨在提高效率的基本原理下破坏对象的方法上。我做了这样的评论:

  1. 在销毁每个对象时显式调用垃圾回收会降低性能,因为这样做没有考虑到 CLR 性能是否绝对必要。
  2. 以该顺序调用这些指令会导致每个对象仅在其他对象正在完成时被销毁。因此,一个可以独立销毁的对象必须等待另一个对象的销毁,而没有真正的必要性。
  3. 会产生死锁(见:this question

1、2 和 3 是真的吗?你能提供一些参考来支持你的答案吗?

虽然我几乎可以肯定我的言论,但我需要在我的论点中明确说明,以便向我的团队解释为什么这是一个问题。这就是我要求确认和参考的原因。

【问题讨论】:

  • 谁会说这是好代码?这显然是不合适的。
  • 就死锁而言,这只发生在某些情况下。代码是在那些情况下吗?如果没有,#3 不是问题。
  • #1 绝对是一个工厂,根据 MSDN:“可以通过调用 Collect 来强制进行垃圾收集,但大多数情况下应该避免这样做,因为它可能会产生性能问题。” - msdn.microsoft.com/en-us/library/66x5fx1b.aspx
  • @CodesInChaos 也许,但如果不需要该内存,等待 GC 并没有什么坏处,如果需要,GC 本身会更快触发。它已经过微调和优化,几乎涵盖了所有代码场景(在 .NET 4.5 中,它进行了更多调整以涵盖更新的场景),所以在这种情况下,我将依赖 Microsoft 工程师的智慧。
  • Piffle and pish... 尝试在循环中缩放和裁剪包含 10 x 4MB 图像(低至约 150k)的文件夹...如果您在之后不手动调用 GC.Collect()每次迭代你都会得到一个内存不足的异常!......有一个例子,它不仅相关而且需要...... 0.001%的时间? LOL 现在用 50 张图片再试一次,效果会更好吗?

标签: c# .net garbage-collection


【解决方案1】:

简短的回答是:把它拿出来。该代码几乎永远不会提高性能或长期内存使用。

您的所有观点都是正确的。 (它可以产生死锁;这并不意味着它总是。)调用GC.Collect() 将收集所有GC 代的内存。这有两件事。

  • 每次跨所有代收集 - 而不是默认情况下 GC 将执行的操作,即仅在代满时收集。典型的使用将看到 Gen0 的收集频率(大约)是 Gen1 的十倍,而 Gen1 的收集频率(大约)是 Gen2 的十倍。这段代码每次都会收集所有的世代。 Gen0 收集通常低于 100 毫秒; Gen2 可以更长。
  • 它将不可收集的物品推广到下一代。也就是说,每次你强制一个集合并且你仍然有对某个对象的引用时,那个对象将被提升到下一代。通常这种情况很少发生,但像下面这样的代码会更频繁地发生这种情况:

    void SomeMethod()
    { 
     object o1 = new Object();
     object o2 = new Object();
    
     o1.ToString();
     GC.Collect(); // this forces o2 into Gen1, because it's still referenced
     o2.ToString();
    }
    

如果没有GC.Collect(),这两个项目都将在下次有机会时收集。 集合被写入,o2 将在 Gen1 中结束 - 这意味着自动 Gen0 集合不会释放该内存。

还值得注意的是更大的恐怖:在 DEBUG 模式下,GC 的功能不同,并且不会回收仍在作用域内的任何变量(即使在当前方法中稍后未使用它)。所以在DEBUG模式下,上面的代码在调用GC.Collect时甚至不会收集o1,所以o1o2都会被提升。在调试代码时,这可能会导致一些非常不稳定和意外的内存使用。 (this 等文章强调了这种行为。)

编辑:刚刚测试了这种行为,有点讽刺的是:如果你有这样的方法:

void CleanUp(Thing someObject)
{
    someObject.TidyUp();
    someObject = null;
    GC.Collect();
    GC.WaitForPendingFinalizers(); 
}

...然后它不会显式释放 someObject 的内存,即使在 RELEASE 模式下:它会将其提升到下一代 GC。

【讨论】:

  • 最后的示例没有收集是有道理的。对 null 的赋值什么都不做,所以它应该被取出,并且由于没有其他活动使用堆栈,编译器可以使用属于本地的空间,而它在范围内的事实并不重要 - 它可以,但它没有理由这样做。
  • @JonHanna 绝对有道理。我只是做了一个明确的例子,因为原始问题表明存在这种形式的“清理”代码。我认为,为他们创建的每个对象调用 GC.Collect 的开发人员也会编写像我的示例一样的代码,这与预期的完全相反。
  • 我遇到了这个问题(谢谢 Dan),但奇怪的是即使在声明范围内将“someObject”分配为 null ——你会认为这会“取消”它——会阻止它不会在 DEBUG 模式下被 GC。
  • 在我当前的项目中,我们使用大型双精度数组(通常大小为 10k),它们直接进入大型对象堆。根据我的经验,.Net 垃圾收集器(至少 v.4)完全无法管理这些东西,所以我不得不添加 GC.Collect() 来停止使用 2GB 内存的进程并一直将东西交换到磁盘.
  • re then it will explicitly NOT release the memory of someObject, even in RELEASE mode: it'll promote it into the next GC generation. - 尝试在github.com/dotnet/coreclr/issues/15207 测试类似的东西,加上非常有用的github.com/dotnet/coreclr/issues/5826#issuecomment-226574611
【解决方案2】:

有一点很容易理解:每次运行 GC 会自动清理许多对象(比如 10000 个)。在每次销毁后调用它,每次运行会清理大约一个对象。

因为 GC 的开销很大(需要停止和启动线程,需要扫描所有存活的对象)批处理调用是非常可取的。

另外,在每个对象之后清理会产生什么好处?这怎么能比批处理更有效呢?

【讨论】:

  • 很好,您正在为每个对象承担成本,而不是将其分散到许多对象上。
  • +1 部分高开销 - 遍历整个对象图 - 随着内存使用量的增加,通常需要更多时间。如果开发人员由于与高内存使用相关的性能不佳而向应用程序添加GC.Collect() 调用,则性能实际上可能降低
  • 主观评论:OP的同事可能会在所有流上调用Flush,Close,Dispose并将它们另外放入using块中。在每个对象之后收集似乎同样被误导。
【解决方案3】:

您的第 3 点在技术上是正确的,但只有在决赛期间有人锁定时才会发生。

即使没有这种调用,锁定在 finaliser 中也比你这里的更糟糕。

有几次调用GC.Collect() 确实有助于提高性能。

到目前为止,在我的职业生涯中,我已经这样做了 2 次,也许 3 次。 (或者,如果你包括我做的那些,测量结果,然后再次取出,可能大约 5 或 6 次 - 这是你应该始终在做完之后测量的东西)。

如果您在短时间内消耗了数百或数千兆内存,然后在很长一段时间内切换到低得多的内存使用,这可能是一个巨大的甚至是显式收集的重要改进。这就是这里发生的事情吗?

在其他任何地方,它们充其量只会让它变慢并使用更多内存。

【讨论】:

    【解决方案4】:

    在这里查看我的其他答案:

    To GC.Collect or not?

    当您自己调用 GC.Collect() 时可能会发生两件事:您最终会花费 更多 时间进行收集(因为除了手动 GC.Collect( )) 并且您会更长时间地保留内存(因为您将某些东西强制放入不需要去那里的更高阶生成)。换句话说,自己使用 GC.Collect() 几乎总是一个坏主意。

    您想要自己调用 GC.Collect() 的唯一一次是当您拥有垃圾收集器难以知道的有关程序的特定信息时。典型示例是一个长时间运行的程序,具有明显的繁忙和轻负载周期。您可能希望在繁忙周期之前的轻负载周期快结束时强制收集,以确保资源对于繁忙周期尽可能空闲。但即使在这里,您可能会发现通过重新思考应用程序的构建方式会做得更好(即,计划任务会更好地工作吗?)。

    【讨论】:

    • 这是某种“应用程序范围”的场景。在发布的场景中,引用了单个“对象销毁”,这意味着扫描所有代,并等待应用程序中所有可能的终结器清理可能与它们无关的单个对象。
    【解决方案5】:

    我们遇到了与@Grzenio 类似的问题,但是我们正在处理更大的二维数组,大约为 1000x1000 到 3000x3000,这是在 web 服务中。

    添加更多内存并不总是正确的答案,您必须了解您的代码和用例。如果没有 GC 收集,我们需要 16-32gb 的内存(取决于客户大小)。如果没有它,我们将需要 32-64gb 的内存,即使这样也不能保证系统不会受到影响。 .NET 垃圾收集器并不完美。

    我们的网络服务有一个内存缓存,大约有 5-50 百万个字符串(每个键/值对约 80-140 个字符,具体取决于配置),此外,对于每个客户端请求,我们将构建 2 个矩阵之一, 一个布尔值,然后传递给另一个服务来完成工作。对于 1000x1000 的“矩阵”(二维数组),这约为 25mb,每个请求。布尔值会说明我们需要哪些元素(基于我们的缓存)。每个缓存条目代表“矩阵”中的一个“单元”。

    当服务器由于分页而具有 > 80% 的内存利用率时,缓存性能会急剧下降。

    我们发现,除非我们明确地进行 GC,否则 .net 垃圾收集器永远不会“清理”临时变量,直到我们处于 90-95% 的范围内,此时缓存性能已急剧下降。

    由于下游过程通常需要很长时间(3-900 秒),GC 收集的性能影响可以忽略不计(每次收集 3-10 秒)。我们在我们已经将响应返回给客户之后发起了这个收集。

    最终我们使 GC 参数可配置,同样在 .net 4.6 中还有更多选项。这是我们使用的 .net 4.5 代码。

    if (sinceLastGC.Minutes > Service.g_GCMinutes)
    {
         Service.g_LastGCTime = DateTime.Now;
         var sw = Stopwatch.StartNew();
         long memBefore = System.GC.GetTotalMemory(false);
         context.Response.Flush();
         context.ApplicationInstance.CompleteRequest();
         System.GC.Collect( Service.g_GCGeneration, Service.g_GCForced ? System.GCCollectionMode.Forced : System.GCCollectionMode.Optimized);
         System.GC.WaitForPendingFinalizers();
         long memAfter = System.GC.GetTotalMemory(true);
         var elapsed = sw.ElapsedMilliseconds;
         Log.Info(string.Format("GC starts with {0} bytes, ends with {1} bytes, GC time {2} (ms)", memBefore, memAfter, elapsed));
    }
    

    在为 .net 4.6 重写后,我们将垃圾收集分为两个步骤 - 一个简单的收集和一个压缩收集。

        public static RunGC(GCParameters param = null)
        {
            lock (GCLock)
            {
                var theParams = param ?? GCParams;
                var sw = Stopwatch.StartNew();
                var timestamp = DateTime.Now;
                long memBefore = GC.GetTotalMemory(false);
                GC.Collect(theParams.Generation, theParams.Mode, theParams.Blocking, theParams.Compacting);
                GC.WaitForPendingFinalizers();
                //GC.Collect(); // may need to collect dead objects created by the finalizers
                var elapsed = sw.ElapsedMilliseconds;
                long memAfter = GC.GetTotalMemory(true);
                Log.Info($"GC starts with {memBefore} bytes, ends with {memAfter} bytes, GC time {elapsed} (ms)");
    
            }
        }
    
        // https://msdn.microsoft.com/en-us/library/system.runtime.gcsettings.largeobjectheapcompactionmode.aspx
        public static RunCompactingGC()
        {
            lock (CompactingGCLock)
            {
                var sw = Stopwatch.StartNew();
                var timestamp = DateTime.Now;
                long memBefore = GC.GetTotalMemory(false);
    
                GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
                GC.Collect();
                var elapsed = sw.ElapsedMilliseconds;
                long memAfter = GC.GetTotalMemory(true);
                Log.Info($"Compacting GC starts with {memBefore} bytes, ends with {memAfter} bytes, GC time {elapsed} (ms)");
            }
        }
    

    希望这对其他人有帮助,因为我们花了很多时间研究这个。

    [编辑] 跟进这个,我们发现大矩阵有一些额外的问题。我们已经开始遇到沉重的内存压力并且应用程序突然无法分配数组,即使进程/服务器有足够的内存(24gb 可用)。经过深入调查,我们发现该进程的备用内存几乎是“正在使用的内存”的 100%(24gb 正在使用,24gb 备用,1gb 空闲)。当“空闲”内存达到 0 时,应用程序将暂停 10 多秒,而待机被重新分配为空闲,然后它可以开始响应请求。

    根据我们的研究,这似乎是由于大型对象堆的碎片。

    为了解决这个问题,我们采取了两种方法:

    1. 我们将更改为锯齿状数组与多维数组。这将减少所需的连续内存量,并在理想情况下将更多这些数组保留在大对象堆之外。
    2. 我们将使用 ArrayPool 类来实现数组。

    【讨论】:

      【解决方案6】:

      我只用过一次:清理 Crystal Report 文档的服务器端缓存。在Crystal Reports Exception: The maximum report processing jobs limit configured by your system administrator has been reached中查看我的回复

      WaitForPendingFinalizers 对我特别有帮助,因为有时对象没有被正确清理。考虑到网页中报告的性能相对较慢 - 任何轻微的 GC 延迟都可以忽略不计,内存管理的改进为我提供了一个整体更快乐的服务器。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2019-12-18
        • 1970-01-01
        • 2019-10-25
        • 2011-09-28
        • 2012-05-11
        • 1970-01-01
        • 1970-01-01
        • 2011-07-20
        相关资源
        最近更新 更多