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

我已经开始查看项目中的一些代码,发现这样的事情:

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

这些线通常出现在被设想在提高效率的理论基础上破坏对象的方法上。 我发表了这样的言论:

  1. 在每个对象的销毁上显式调用垃圾收集会降低性能,因为如果CLR性能绝对必要,则不考虑它。
  2. 按该顺序调用这些指令会导致每个对象仅在最终确定其他对象时被销毁。 因此,一个可以独立销毁的对象必须等待其他对象的破坏而没有必要。
  3. 它可以产生死锁(参见: 这个问题 )

1,2和3都是真的吗? 你能提供一些支持你答案的参考资料吗?

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

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

你的所有观点都是正确的。 (它可以生成死锁;这并不意味着它总是 。)调用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的function不同,并且不会回收任何仍在范围内的变量(即使它在当前方法中未被使用)。 因此在DEBUG模式下,上面的代码在调用GC.Collect时甚至不会收集o1 ,因此o1o2都会被提升。 调试代码时,这可能会导致一些非常不稳定和意外的内存使用。 (诸如此类的文章突出了这种行为。)

编辑:刚刚测试过这种行为,有些讽刺:如果你有一个像这样的方法:

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

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

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

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

此外,每个物体清理后会有什么好处 ? 这怎么可能比批处理更有效?

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

即使没有这种呼叫,锁定终结器内部甚至比你在这里更糟糕。

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

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

如果你在短时间内通过数百或数千兆内存,然后长时间切换到更少密集的内存使用,它可能是一个巨大的甚至是重要的改进明确收集。 那是在发生什么事吗?

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

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

要GC.Collect与否?

当你自己调用GC.Collect()时会发生两件事:你最终会花更多的时间去做集合(因为正常的背景集合除了你的手册GC.Collect()之外还会发生)你会坚持到记忆时间更长 (因为你强迫某些东西进入更高阶的生成而不需要去那里)。 换句话说,自己使用GC.Collect()几乎总是一个坏主意。

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

我只使用过一次:清理Crystal Report文档的服务器端缓存。 请参阅Crystal Reportsexception中的响应:已达到系统管理员配置的最大报表处理作业限制

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

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

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

我们的web服务有一个内存缓存,大小为5-50万字符串(每个键/值对约80-140个字符,具体取决于配置),另外每个客户端请求我们将构造2个矩阵之一,其中一个然后将布尔值传递给另一个服务来完成工作。 对于1000×1000“矩阵”(2维arrays), 每个请求约为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)"); } } 

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