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

208

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

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

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

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


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

5

尽可能避免使用GC.Collect(),因为其代价很高。以下是一个例子:

        public void ClearFrame(ulong timeStamp)
    {
        if (RecordSet.Count <= 0) return;
        if (Limit == false)
        {
            var seconds = (timeStamp - RecordSet[0].TimeStamp)/1000;
            if (seconds <= _preFramesTime) return;
            Limit = true;
            do
            {
                RecordSet.Remove(RecordSet[0]);
            } while (((timeStamp - RecordSet[0].TimeStamp) / 1000) > _preFramesTime);
        }
        else
        {
            RecordSet.Remove(RecordSet[0]);

        }
        GC.Collect(); // AVOID
    }

测试结果:CPU使用率为12%

当您更改为此:

        public void ClearFrame(ulong timeStamp)
    {
        if (RecordSet.Count <= 0) return;
        if (Limit == false)
        {
            var seconds = (timeStamp - RecordSet[0].TimeStamp)/1000;
            if (seconds <= _preFramesTime) return;
            Limit = true;
            do
            {
                RecordSet[0].Dispose(); //  Bitmap destroyed!
                RecordSet.Remove(RecordSet[0]);
            } while (((timeStamp - RecordSet[0].TimeStamp) / 1000) > _preFramesTime);
        }
        else
        {
            RecordSet[0].Dispose(); //  Bitmap destroyed!
            RecordSet.Remove(RecordSet[0]);

        }
        //GC.Collect();
    }

测试结果:CPU使用率为2-3%


4
在你的例子中,我认为调用GC.Collect不是问题所在,而是存在设计问题。
如果您要在间隔时间(设置时间)内唤醒,则应编写单个执行的程序(执行一次任务),然后终止该程序。然后,将程序设置为定期运行的计划任务,以在预定间隔运行。
这样,您就不必担心调用GC.Collect(如果有必要,也应该很少这样做)。
话虽如此,Rico Mariani在这个主题上有一篇很棒的博客文章,可以在这里找到:

http://blogs.msdn.com/ricom/archive/2004/11/29/271829.aspx


4

一个调用GC.Collect()的有用场景是在单元测试中,当您想要验证您没有创建内存泄漏时(例如如果您正在使用WeakReferences或ConditionalWeakTable、动态生成的代码等),您可以这样做。

例如,我有一些像这样的测试:

WeakReference w = CodeThatShouldNotMemoryLeak();
Assert.IsTrue(w.IsAlive);
GC.Collect();
GC.WaitForPendingFinalizers();
Assert.IsFalse(w.IsAlive);

可以说使用弱引用本身就是一个问题,但似乎如果您正在创建依赖于此行为的系统,则调用GC.Collect()是验证此类代码的一种好方法。


3
有些情况下,小心谨慎比后悔莫及更明智。以下是一个这样的情况。
有时需要使用IL重写在C#中编写“非托管”DLL(因为有些情况下这是必要的)。例如,假设DLL在类级别创建了一个字节数组 - 因为许多导出函数需要访问该数组。当DLL被卸载时会发生什么?垃圾收集器会自动调用吗?我不知道,但由于这是一个“非托管”的DLL,完全有可能垃圾收集器未被调用。如果没有调用,将会是一个大问题。当DLL被卸载时,垃圾收集器也将被卸载, 那么谁来负责收集任何可能的垃圾并且如何收集呢?最好使用C#的垃圾收集器。提供一个清除功能(对DLL客户端可用)来将类级变量设置为null并调用垃圾收集器。
谨慎小心总比后悔莫及好。

这个人说得有道理,这就是为什么所有这些“不要”的论点大多都是无关紧要的原因。当你使用一个非托管库时,你所拥有的只是对非托管对象的托管指针。由于垃圾回收器将这些指针视为极小的,它不会急于收集它们。这就是为什么在实现IDisposable的非托管对象上进行Dispose非常重要的原因。 - codinglifestyle

3

如果您正在创建大量的新System.Drawing.Bitmap对象,则垃圾回收器不会清除它们。最终,GDI+将认为您的内存不足,并抛出“参数无效”的异常。定期(不要太频繁!)调用GC.Collect()似乎可以解决此问题。


1
在完成使用GDI+对象后调用Dispose()是基本的Winforms编程。 - Ian Ringrose

2
using(var stream = new MemoryStream())
{
   bitmap.Save(stream, ImageFormat.Png);
   techObject.Last().Image = Image.FromStream(stream);
   bitmap.Dispose();

   // Without this code, I had an OutOfMemory exception.
   GC.Collect();
   GC.WaitForPendingFinalizers();
   //
}

2
简短回答是:永远不要!

10
甚至在这种情况下,也不能实现取消未使用的内存。 - Roman Starkov

2
另一个原因是,当您在USB COM端口上打开了SerialPort,并且然后拔掉了USB设备。由于SerialPort已经打开,资源会在系统注册表中保留对先前连接的端口的引用。系统注册表将包含陈旧的数据,因此可用端口列表将是错误的。因此,必须关闭该端口。
在端口上调用SerialPort.Close()会调用对象上的Dispose()方法,但它仍然驻留在内存中,直到垃圾回收实际运行,导致注册表保持陈旧,直到垃圾回收器决定释放该资源。
来自https://stackoverflow.com/a/58810699/8685342。
try
{
    if (port != null)
        port.Close(); //this will throw an exception if the port was unplugged
}
catch (Exception ex) //of type 'System.IO.IOException'
{
    System.GC.Collect();
    System.GC.WaitForPendingFinalizers();
}

port = null;

1

在一些内存较小的 ARM 计算机上,例如运行 mono 的 Raspberry PI 上,调用垃圾回收器的一个很好的理由是:如果未分配的内存片段占用了太多系统内存,则 Linux 操作系统可能会变得不稳定。我有一个应用程序,需要每秒钟调用 GC 一次,以解决内存溢出问题。

另一个很好的解决方案是,在不再需要时处理对象。不幸的是,在许多情况下这并不容易。


1
我对此仍然有些不确定。我在一个应用服务器上工作已经七年了。我们更大的安装需要使用 24 GB RAM。它高度多线程化,所有 GC.Collect() 的调用都会导致性能问题。
许多第三方组件在他们认为现在这样做很聪明时使用了 GC.Collect()。因此,一堆简单的 Excel 报表每分钟都会阻塞应用服务器的所有线程几次。
我们不得不重构所有第三方组件以删除 GC.Collect() 调用,之后一切正常。
但是,在 Win32 上运行服务器时,我也开始大量使用 GC.Collect(),因为遇到 OutOfMemoryException。
但我对此也不太确定,因为我经常注意到,当我在 32 位上遇到 OOM,并且在不调用 GC.Collect() 的情况下重试运行相同的操作时,它就正常工作了。
我想知道的一件事是 OOM 异常本身……如果我编写了 .Net Framework,而且无法分配内存块,我会使用 GC.Collect(),整理内存(??),再尝试一次,如果仍然找不到空闲内存块,那么我将抛出 OOM 异常。

希望至少将此行为作为可配置选项,由于GC.Collect的性能问题缺点。

现在我的应用程序中有很多这样的代码来“解决”这个问题:

public static TResult ExecuteOOMAware<T1, T2, TResult>(Func<T1,T2 ,TResult> func, T1 a1, T2 a2)
{

    int oomCounter = 0;
    int maxOOMRetries = 10;
    do
    {
        try
        {
            return func(a1, a2);
        }
        catch (OutOfMemoryException)
        {
            oomCounter++;
            if (maxOOMRetries > 10)
            {
                throw;
            }
            else
            {
                Log.Info("OutOfMemory-Exception caught, Trying to fix. Counter: " + oomCounter.ToString());
                System.Threading.Thread.Sleep(TimeSpan.FromSeconds(oomCounter * 10));
                GC.Collect();
            }
        }
    } while (oomCounter < maxOOMRetries);

    // never gets hitted.
    return default(TResult);
}

请注意,Thread.Sleep()的行为是一个非常特定于应用程序的行为,因为我们正在运行ORM缓存服务,并且如果RAM超过一些预定义值,则该服务需要一些时间来释放所有缓存的对象。因此,第一次等待几秒钟,并且每次OOM出现时等待时间都增加。


一个组件不应调用GC.Collect。因为它具有应用程序范围的效果,只有应用程序才应该这样做(如果需要的话)。 - CodesInChaos
如果我写了 .Net Framework,而且我不能分配内存块,我会使用 GC.Collect()。我认为他们已经在做这个了——我看到有迹象表明,内部 GC 触发器之一是某些内存分配失败。 - G. Stoynev

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