什么情况下可以调用GC.Collect方法?

208

一般建议不要在代码中调用GC.Collect,但有哪些特例情况呢?

我只能想到几种非常特殊的情况,在这些情况下强制进行垃圾回收可能是有意义的。

其中一个例子是服务会以间隔时间唤醒,执行一些任务,然后休眠很长时间。在这种情况下,强制进行垃圾回收可能是个好主意,以防止即将变得空闲的进程占用比需要更多的内存。

除了这种情况之外,还有什么其他情况可以接受调用GC.Collect吗?


Scott Holden的关于何时(以及何时不)调用GC.Collect的博客文章是针对.NET Compact Framework的,但这些规则通常适用于所有托管开发。 - ctacke
可能是在C#中强制进行垃圾回收的最佳实践的重复内容。 - Jimenemex
23个回答

176
如果您有充分的理由相信一组重要的对象(特别是那些您认为位于第1代和第2代中)现在可以进行垃圾回收,并且现在是适当的时间进行回收,以小的性能损失为代价,那么这是一个很好的例子,例如,如果您刚刚关闭了一个大型表单,则知道现在可以对所有UI控件进行垃圾回收,并且在关闭表单时短暂的停顿可能不会被用户注意到。
更新 2018 年 2 月 7 日
从 .NET 4.5 开始 - 有 GCLatencyMode.LowLatency 和 GCLatencyMode.SustainedLowLatency。在进入和离开这两种模式时,建议使用 GC.Collect(2, GCCollectionMode.Forced) 强制进行完整 GC。
从 .NET 4.6 开始 - 有 GC.TryStartNoGCRegion 方法(用于设置只读值 GCLatencyMode.NoGCRegion)。这本身可以执行完全阻止垃圾回收以尝试释放足够的内存,但考虑到我们在一段时间内不允许 GC,我认为在之前和之后执行完整的 GC 也是一个好主意。
来源:微软工程师 Ben Watson 的《编写高性能 .NET 代码》,第二版,2018年。
请参见:

8
根据微软源代码,每850毫秒调用GC.Collect(2)是完全可以的。不信?那就看看PresentationCore.dll,MS.Internal.MemoryPressure.ProcessAdd()。我现在有一个图像处理应用程序(小图像,没有真正的内存压力),调用GC.Collect(2)所需时间超过了850毫秒,因此整个应用程序会被冻结(应用程序99.7%的时间花费在GC上)。 - springy76
42
微软在某个地方推出的做法并不意味着那些来自微软内部提供建议的人认为这是一件好事情。 - Jon Skeet
4
我不喜欢那个例子。在表单关闭后执行它有什么意义?我看到一个好的例子是在XBox或WindowsPhone上加载游戏关卡后运行GC(垃圾回收),在分配1MB或类似大小的内存后运行。因此,在加载关卡时尽可能多地分配内存(同时显示一些启动画面),然后使用GC.Collect尝试避免游戏期间的垃圾回收。 - Piotr Perak
8
在表单关闭后执行这个操作的目的在于,你刚刚创建了大量的对象(控件、数据等),它们都可以被垃圾回收。因此,调用 GC.Collect 就相当于告诉垃圾回收器你比它更懂得如何清理内存。你为什么不喜欢这个例子呢? - Jon Skeet
7
@SHCJ: GC.Collect()会请求GC执行完整的垃圾回收。如果您知道您刚刚使许多以前长期存在的对象有资格进行垃圾回收,并且您认为用户现在比以后更不可能注意到轻微的暂停,那么现在似乎是促使回收的更好时机,而不是让它以后发生。 - Jon Skeet
显示剩余7条评论

55

我只在编写简单的性能/分析测试时使用GC.Collect;例如,我有两个(或更多)要测试的代码块,类似于:

GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced);
TestA(); // may allocate lots of transient objects
GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced);
TestB(); // may allocate lots of transient objects
GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced);
...

为了使 TestA()TestB() 执行时状态尽可能相似——即 TestA() 接近临界点时,TestB() 不会受到太多影响。

一个经典的例子是一个简单的控制台应用程序(例如可以在这里发布的 Main 方法),它展示了循环字符串连接和 StringBuilder 之间的差异。

如果我需要精确的结果,那么这将是两个完全独立的测试——但通常只需最小化(或标准化)测试期间的垃圾回收以获得行为的大致感觉。

在生产代码中? 我还没有使用它 ;-p


1
在这种情况下,我可能还会添加“WaitForPendingFinalizers”(或类似的内容);-p - Marc Gravell

34
大多数情况下最好不要强制进行垃圾回收。(我所接触的每个需要强制进行垃圾回收的系统,都存在基本问题,如果解决这些问题,就可以消除对强制垃圾回收的需求并大大提高系统速度。) 只有在一些特定情况下,您才会比垃圾收集器更了解内存使用情况。但这在多用户应用程序或同时响应多个请求的服务中不太可能发生。
然而,在一些批处理类型的应用程序中,您确实比GC更了解情况。例如,请考虑一个应用程序:
- 在命令行上给出文件名列表 - 处理单个文件,然后将结果写入结果文件。 - 在处理文件时,创建许多相互关联的对象,直到文件处理完成后才能进行收集(例如,解析树)。 - 不保留已处理过的文件之间的匹配状态
在经过仔细测试后,您可能会认为,在处理完每个文件后强制进行完整的垃圾回收是有意义的。
另一个例子是每隔几分钟唤醒以处理某些项目的服务,并且在其进入睡眠状态时不保留任何状态。然后,在进入睡眠状态之前强制进行完整的垃圾回收可能是值得的。

我唯一会考虑强制回收的情况是当我知道最近创建了大量对象并且当前只引用了很少的对象时。

我更希望有一个垃圾回收API,可以在不强制GC的情况下提供关于这种情况的提示。
另请参见“Rico Mariani的性能小贴士

最近我认为使用短暂的工作进程来处理每批工作,并让操作系统进行资源回收会更好,与上述情况类似。


24

有一种情况是当您尝试对使用WeakReference的代码进行单元测试时。


16

在大型 24/7 或 24/6 系统中——响应消息、RPC 请求或连续轮询数据库或进程的系统——有一种识别内存泄漏的方法很有用。为此,我倾向于在应用程序中添加机制,将任何处理暂时挂起,然后执行完整的垃圾回收。这将使系统进入静止状态,在该状态下,剩余的内存要么是合法的长期内存(缓存、配置等),要么是“泄漏”的(不希望或预期成为根对象但实际上确实是的对象)。

拥有此机制可大大简化性能分析过程,因为报告不会被活动处理中的噪音干扰。

为确保获取所有垃圾,需要执行两次垃圾回收:

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

由于第一个集合将导致任何具有终结器的对象被终止(但不会实际回收这些对象),因此第二个垃圾回收将回收这些已经终止的对象。


我现在已经在几个地方看到了双遍历集合的方法,但是在阅读了MSDN文档中GC.WaitForPendingFinalizers的段落之后,我有些担心。其中写道:“在继续执行之前,请等待所有终结器完成。如果没有调用GC.WaitForPendingFinalizers,下面的工作循环可能会与终结器同时执行。通过调用此函数,工作循环只有在所有终结器被调用后才会执行。”你知道有没有权威的资料来进行双遍历吗? - jerhewet
1
@jerhewet:理解为什么需要两个集合的关键在于理解具有终结器的对象会发生什么。不幸的是,我没有你要求的确切内容,但请阅读此文章和此SO问题。 - Paul Ruane

12

当你了解应用程序的性质,而垃圾回收器(GC)无法了解时,你可以调用 GC.Collect()

作为作者,经常会觉得这很可能或很正常。然而,事实上,GC 是一个编写和测试相当不错的专家系统,你很少会知道它没有关于低级代码路径的信息。

我能想到的最好的例子是在空闲期和非常繁忙的时期之间循环的应用程序。你希望在繁忙期间获得最佳性能,因此想利用闲置时间进行一些清理工作。

然而,大多数情况下,GC 已经足够智能,可以自动完成这项工作。


9
在通过Interop自动化Microsoft Office时,几乎必须调用GC.Collect()方法,因为Office的COM对象不会自动释放,这可能导致Office产品实例占用大量内存。我不确定这是问题还是设计如此。因为互联网上有很多关于此主题的帖子,所以我不会讲得太详细。
在使用Interop编程时,每个COM对象都应该手动释放,通常使用Marshal.ReleseComObject()方法进行释放。此外,手动调用垃圾回收可以帮助"清理"一些东西。当你完成Interop对象后,调用以下代码似乎可以帮助很多:
GC.Collect()
GC.WaitForPendingFinalizers()
GC.Collect()

根据我的个人经验,使用ReleaseComObject和手动调用垃圾回收相结合可以极大地减少Office产品,尤其是Excel的内存使用。


是的,我在使用.NET访问Excel时也遇到了这个问题,因为它也是通过COM对象工作的。需要注意的是,在DEBUG模式下,GC操作受到限制,因此这种方法效果不佳。只有在RELEASE模式下才能正常工作。相关链接:https://dev59.com/pmQn5IYBdhLWcg3wETk5#17131389 - Welcor

8
作为内存碎片化的解决方案。 在向内存流中写入大量数据时(从网络流读取),我遇到了内存不足异常。数据以8K块写入。在达到128M后,即使有很多可用内存(但是它是碎片化的),也会出现异常。调用GC.Collect()解决了这个问题。修复后,我能够处理超过1G的数据。

我相信随着 .Net GC 系统的更新,这个问题已经得到解决。 - Ian Ringrose

7

请看Rico Mariani的这篇文章,他给出了两个关于何时调用GC.Collect的规则(第一个规则是:“不要”):

何时调用GC.Collect()


4
已经有了这种经历。我并不是要找借口去做本不该做的事,但我想知道是否存在特殊情况下可以接受的情况。 - Brian Rasmussen
1
何时调用GC.Collect? - Кое Кто

6

我正在对数组和列表进行一些性能测试:

private static int count = 100000000;
private static List<int> GetSomeNumbers_List_int()
{
    var lstNumbers = new List<int>();
    for(var i = 1; i <= count; i++)
    {
        lstNumbers.Add(i);
    }
    return lstNumbers;
}
private static int[] GetSomeNumbers_Array()
{
    var lstNumbers = new int[count];
    for (var i = 1; i <= count; i++)
    {
        lstNumbers[i-1] = i + 1;
    }
    return lstNumbers;
}
private static int[] GetSomeNumbers_Enumerable_Range()
{
    return  Enumerable.Range(1, count).ToArray();
}

static void performance_100_Million()
{
    var sw = new Stopwatch();

    sw.Start();
    var numbers1 = GetSomeNumbers_List_int();
    sw.Stop();
    //numbers1 = null;
    //GC.Collect();
    Console.WriteLine(String.Format("\"List<int>\" took {0} milliseconds", sw.ElapsedMilliseconds));

    sw.Reset();
    sw.Start();
    var numbers2 = GetSomeNumbers_Array();
    sw.Stop();
    //numbers2 = null;
    //GC.Collect();
    Console.WriteLine(String.Format("\"int[]\" took {0} milliseconds", sw.ElapsedMilliseconds));

    sw.Reset();
    sw.Start();
//getting System.OutOfMemoryException in GetSomeNumbers_Enumerable_Range method
    var numbers3 = GetSomeNumbers_Enumerable_Range();
    sw.Stop();
    //numbers3 = null;
    //GC.Collect();

    Console.WriteLine(String.Format("\"int[]\" Enumerable.Range took {0} milliseconds", sw.ElapsedMilliseconds));
}

我在 GetSomeNumbers_Enumerable_Range 方法中遇到了 OutOfMemoryException,唯一的解决方法是通过以下方式释放内存:

numbers = null;
GC.Collect();

1
为什么要踩我的回答?我的回答是一个示例,演示了何时调用GC。你有更好的建议吗?欢迎提出。 - Daniel B

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