使用GC.Collect()是否正确; GC.WaitForPendingFinalizers();?

时间:2012-09-04 14:26:19

标签: c# .net garbage-collection

我已经开始审查项目中的一些代码并发现了类似的内容:

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

这些线通常出现在被设想在提高效率的基础上破坏对象的方法上。我已经发表了这番话:

  1. 在销毁每个对象时显式调用垃圾收集会降低性能,因为这样做不会考虑CLR性能是否绝对必要。
  2. 按顺序调用这些指令会导致每个对象仅在最终确定其他对象时被销毁。因此,一个可以独立销毁的对象必须等待另一个对象的破坏而没有必要。
  3. 它可能会产生死锁(请参阅:this question
  4. 1,2和3是真的吗?你能给出一些支持你答案的参考吗?

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

6 个答案:

答案 0 :(得分:28)

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

你的所有观点都是正确的。 (它可以生成死锁;这并不意味着它总是。)调用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(),则会在下一次机会时收集这两个项目。 集合作为writte,o2将在Gen1中结束 - 这意味着自动Gen0集合将不会释放该内存。

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

编辑:刚刚测试过这种行为,有些讽刺:如果你有类似的方法:

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

...然后它会明确地释放someObject的内存,即使在RELEASE模式下:它会将它推广到下一代GC。

答案 1 :(得分:8)

有一点可以让人很容易理解:GC运行会自动清理每次运行的许多对象(比如10000)。每次破坏后调用它会清除每次运行一个对象。

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

此外,在每个对象之后清理可能会出现什么?这怎么可能比批处理更有效?

答案 2 :(得分:6)

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

即使没有这种电话,锁定在终结者中也比你在这里更糟糕。

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

到目前为止,我已经完成了2次,也许是我职业生涯中的3次。 (或者如果你包括我做过的那些,可能大约5到6次,测量结果,然后再把它拿出来 - 这是你应该总是测量后的事情)。

如果你在很短的时间内翻过几百或几千兆的内存,然后在很长一段时间内切换到不那么密集的内存使用,它可能是一个巨大的甚至是大量的显着收集的重要改进。那是在发生什么事吗?

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

答案 3 :(得分:5)

请在此处查看我的其他答案:

<强> To GC.Collect or not?

  当你自己调用GC.Collect()时,可能会发生两件事情:你最终花费更多时间进行收集(因为除了手册GC.Collect之外,正常的背景收集仍会发生) ))并且你将继续留在内存更长(因为你强迫某些东西进入更高阶的生成而不需要去那里)。换句话说,自己使用GC.Collect()几乎总是一个坏主意。

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

答案 4 :(得分:2)

我只使用过一次:清理Crystal Report文档的服务器端缓存。请参阅Crystal Reports Exception: The maximum report processing jobs limit configured by your system administrator has been reached

中的回复

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

答案 5 :(得分:2)

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

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

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

当服务器具有&gt;时,缓存性能会急剧下降。分页导致80%的内存利用率。

我们发现,除非我们明确GC,否则.net垃圾收集器永远不会清理&#39;暂时变量,直到我们处于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)");
        }
    }

希望这可以帮助别人,因为我们花了很多时间研究这个。