在C#中强制垃圾回收的最佳实践


118

根据我的经验,似乎大多数人会告诉您,强制进行垃圾回收是不明智的,但是在某些情况下,如果您使用的大型对象并非总是在0代中被收集,而内存是一个问题,可以强制收取吗?有没有这样做的最佳实践?

Answers:


112

最佳做法是不要强制垃圾回收。

根据MSDN:

“可以通过调用Collect来强制进行垃圾收集,但是在大多数情况下,应避免这种情况,因为这可能会导致性能问题。”

但是,如果可以可靠地测试代码以确认调用Collect()不会产生负面影响,请继续...

只需尝试确保在不再需要对象时清理它们。如果您有自定义对象,请考虑使用“ using语句”和IDisposable接口。

该链接在释放内存/垃圾回收等方面有一些很好的实用建议:

http://msdn.microsoft.com/zh-CN/library/66x5fx1b.aspx


3
另外,您可以设置不同的LatencyMode的msdn.microsoft.com/zh-cn/library/bb384202.aspx
David d C e Freitas

4
如果您的对象指向非托管内存,则可以通过GC.AddMemoryPressure Api(msdn.microsoft.com/en-us/library/…)让垃圾收集器知道。这为垃圾收集器提供了有关系统的更多信息,而不会干扰收集算法。
Govert,2012年

+1:*使用“ using语句”和IDisposable接口。*我什至不考虑强制使用,除非是万不得已-好的建议(读作“免责声明”)。但是,我要在单元测试中强制收集以模拟在后端操作中丢失活动的引用-最终抛出TargetOfInvocationNullException
IAbstract 2012年

33

这样看-在垃圾桶为10%时将厨房垃圾扔掉或在取出之前让其填满是否更有效率?

通过不让它填满,您在浪费时间往返于外面的垃圾箱。这类似于GC线程运行时发生的事情-所有托管线程在运行时都将挂起。而且,如果我没记错的话,GC线程可以在多个AppDomain之间共享,因此垃圾回收会影响所有这些线程。

当然,您可能会遇到这样的情况,即您很快就不会在垃圾桶中添加任何东西-例如,如果您打算休假。然后,最好在出门前先扔掉垃圾。

这可能是强制GC可以提供帮助的一倍-如果程序空闲,则由于没有分配,因此不会对正在使用的内存进行垃圾回收。


5
如果您有一个婴儿,如果将其放置超过一分钟会死亡,并且您只有一分钟的时间来处理垃圾,那么您希望每次都做一点而不是一次全部。不幸的是,您经常调用GC :: Collect()方法并没有更快。因此,对于实时引擎而言,如果不能仅使用处理机制并让GC池化数据,那么就不应该使用受管系统-根据我的回答(可能低于此,大声笑)。
Jin

1
就我而言,我正在重复运行A *(最短路径)算法以对其进行性能调整...在生产中,该算法仅运行一次(在每个“地图”上)。因此,我希望在每次迭代之前都在我的“性能度量模块”之外完成GC,因为我觉得可以更紧密地模拟生产情况,在这种情况下,可以/应该在导航每个“映射”之后强制使用GC。
corlettk 2012年

实际上,在被测块之前调用GC不会对生产情况进行建模,因为在生产中,GC将在不可预测的时间内完成。为了减轻这种情况,您应该进行长时间的测量,其中包括执行多个GC,并将GC期间的峰纳入分析和统计数据中。
2012年

32

最佳做法是在大多数情况下不要强制垃圾回收。 (我研究过的每个系统都强制执行垃圾收集,并强调了以下问题:如果解决了这些问题,将消除了强制执行垃圾收集的需求,并大大加快了系统运行速度。)

在某些情况下比垃圾收集器了解更多有关内存使用的信息。在多用户应用程序或一次响应一个以上请求的服务中,这不太可能是正确的。

但是,在某些批处理中,您对GC的了解更多。例如考虑一个应用程序。

  • 在命令行上获得文件名列表
  • 处理单个文件,然后将结果写到结果文件中。
  • 在处理文件时,会创建很多互连的对象,这些对象在文件处理完成之前无法收集(例如,解析树)
  • 在已处理的文件之间不保留太多状态

可能可以进行案例分析(经过仔细的测试),在处理完每个文件后应强制执行完整的垃圾回收。

另一种情况是,该服务每隔几分钟醒来以处理一些项目,并且在睡眠时不保持任何状态。然后在临睡前强制进行一次完整的收集可能是值得的。

我唯一考虑强制收集的时间是当我知道最近创建了很多对象并且当前仅引用了很少的对象时。

当我可以向我提供有关此类事情的提示而不必强迫自己使用GC时,我宁愿拥有一个垃圾收集API。

另请参阅“ Rico Mariani的演奏花絮


2
打个比方:Playschool(系统)保留蜡笔(资源)。根据孩子的数量(任务)和颜色的稀缺性,老师(.Net)决定如何在孩子之间分配和共享。当请求稀有颜色时,教师可以从池中分配或查找未使用的颜色。教师可以自行决定定期收集未使用的蜡笔(垃圾收集),以保持物品整洁(优化资源使用)。通常,父母(程序员)无法确定最佳教室蜡笔整理政策。一个孩子预定的午睡不太可能是干扰其他孩子颜色的好时机。
AlanK

1
@AlanK,我喜欢这样,因为当孩子们整天回家时,这是一个很好的时机,可以帮助老师的助手进行整洁,而不会让孩子迷路。(我正在使用的最新系统刚刚在这种情况下重新启动了服务进程,强制执行了GC。)
Ian

21

我认为Rico Mariani给出的示例很好:如果应用程序状态发生重大变化,则可能触发GC。例如,在文档编辑器中,关闭文档时可以触发GC。


2
或者在打开一个大型的连续对象之前,该对象显示了失败的历史并且没有有效的分辨率来提高其粒度。
crokusek

17

几乎没有绝对的通用编程准则。一半时间,当有人说“你做错了”时,他们只是在吐出一定数量的教条。在C语言中,以前担心诸如自修改代码或线程之类的问题,而在GC语言中,它是在强制GC或阻止其运行。

与大多数准则和良好的经验法则(以及良好的设计实践)一样,在极少数情况下,遵循既定准则确实有意义。您必须非常确定自己了解案件,您的案件确实需要废除惯例,并且您了解可能引起的风险和副作用。但是有这种情况。

编程问题千差万别,需要灵活的方法。我已经看到了用垃圾收集的语言阻止GC的情况,以及触发它而不是等待它自然发生的情况。在95%的时间中,这两种情况都表示没有正确解决问题。但是20中有1次可能有一个有效的理由。


12

我学会了不要试图超过垃圾回收。话虽如此,using当处理诸如文件I / O或数据库连接之类的非托管资源时,我只是坚持使用关键字。


27
编译器?编译器与GC有什么关系?:)
KristoferA

1
没什么,它只会编译和优化代码。而且,这与CLR ...甚至.NET毫无关系。
Kon

1
占用大量内存的对象(例如非常大的图像)最终可能无法得到垃圾回收,除非您明确地对其进行垃圾回收。我认为(大对象)或多或少是OP的问题。
code4life

1
将其包装在使用中将确保一旦超出使用范围就将其安排用于GC。除非计算机爆炸,否则很可能会清理该内存。
2013年

9

不知道这是否是最佳实践,但是在循环处理大量图像时(即创建和布置许多Graphics / Image / Bitmap对象),我会定期使用GC.Collect。

我想我读到某个地方说,GC仅在程序(主要是)空闲时才运行,而不是在密集循环的中间运行,因此看起来好像是手动GC有意义的领域。


您确定需要这个吗?即使您的代码不是空闲的,GC 也会在需要内存时进行收集。
康拉德·鲁道夫

不确定现在在.net 3.5 SP1中如何,但是以前(1.1,我相信我已经针对2.0进行了测试),确实在内存使用方面有所不同。当然,GC总是会在需要时收集,但是当您只需要20时,您可能仍然会浪费100 Meg的RAM。尽管如此,还需要进行一些其他测试
Michael Stum

2
当第0代达到某个阈值(例如1MB)时,而不是在“某事空闲”时,将在内存分配时触发GC。否则,您可以通过简单地分配并立即丢弃对象而最终在循环中遇到OutOfMemoryException。
liggett78,

5
如果没有其他进程需要,则不会浪费100meg的RAM。它为您带来了不错的性能提升:-P
Orion

9

我最近遇到的一种情况是需要手动调用,这种情况GC.Collect()是在处理包裹在小型托管C ++对象中的大型C ++对象时进行的,这些对象又可以从C#中进行访问。

垃圾回收器从未被调用,因为使用的托管内存量可以忽略不计,但是使用的非托管内存量却很大。手动调用Dispose()对象要求我跟踪自己何时不再需要对象,而调用GC.Collect()将清除不再引用的任何对象.....


6
解决此问题的更好方法是GC.AddMemoryPressure (ApproximateSizeOfUnmanagedResource)在构造函数中调用,然后GC.RemoveMemoryPressure(addedSize)在终结器中调用。这样,垃圾收集器将自动运行,并考虑到可以收集的非托管结构的大小。stackoverflow.com/questions/1149181/...
HugoRune

解决该问题的一种更好的方法是实际调用Dispose(),无论如何您都应该这样做。
fabspro 2014年

2
最好的方法是使用Using结构。试试/最后。处置是一件麻烦事
TamusJRoyce 2015年

7

我认为您已经列出了最佳实践,除非真正必要,否则不要使用它。我强烈建议您在可能需要首先回答这些问题的情况下,使用分析工具来详细了解您的代码。

  1. 您的代码中是否有某些内容声明的项目范围超出了所需范围
  2. 内存使用率真的过高吗
  3. 在使用GC.Collect()之前和之后比较性能,以查看是否确实有帮助。

5

假设您的程序没有内存泄漏,对象积累并且无法在Gen 0中进行GC处理,因为:1)它们被长时间引用,因此请进入Gen1和Gen2;2)它们是大对象(> 80K),因此请进入LOH(大对象堆)。而且LOH不会像Gen0,Gen1和Gen2那样进行压缩。

检查“ .NET Memory”的性能计数器,是否可以看到1)问题确实不是问题。通常,每10个Gen0 GC将触发1个Gen1 GC,每10个Gen1 GC将触发1个Gen2 GC。从理论上讲,如果GC0上没有压力(如果程序内存使用确实已连接),则GC1和GC2将永远无法进行GC处理。我从来没有发生过。

对于问题2),您可以检查“ .NET内存”性能计数器以验证LOH是否变得肿。如果确实是您遇到的问题,那么可以按照此博客的建议创建一个大对象池,网址http://blogs.msdn.com/yunjin/archive/2004/01/27/63642.aspx


4

大对象是在LOH(大对象堆)上分配的,而不是在gen 0上分配的。如果您说它们没有在gen 0上进行垃圾回收,那么您是对的。我相信只有在整个GC周期(第0、1和2代)发生时才收集它们。

话虽这么说,但我相信另一方面,当您处理大型对象并且内存压力不断增加时,GC会更积极地调整和收集内存。

很难说是否收集以及在什么情况下收集。在处理具有众多控件等的对话框窗口/窗体之后,我曾经做过GC.Collect()。(因为该窗体及其控件最终在第二代中由于创建了许多业务对象实例/加载了很多数据而被淘汰-否显然是大物件),但实际上从长远来看并没有注意到任何正面或负面影响。


4

我想补充一点:调用GC.Collect()(+ WaitForPendingFinalizers())是故事的一部分。正如其他人正确提到的那样,GC.COllect()是非确定性集合,并且由GC本身(CLR)自行决定。即使您添加了对WaitForPendingFinalizers的调用,它也可能不确定。从此msdn 链接中获取代码,然后以对象循环迭代为1或2的方式运行代码。您将发现非确定性的含义(在对象的析构函数中设置断点)。确切地说,当Wait ..()仅存在1(或2)个挥之不去的对象时,不会调用析构函数。[Citation reqd。]

如果您的代码正在处理非托管资源(例如:外部文件句柄),则必须实现析构函数(或终结器)。

这是一个有趣的示例:

注意:如果您已经尝试过MSDN的上述示例,则以下代码将使您大开眼界。

class Program
{    
    static void Main(string[] args)
        {
            SomePublisher publisher = new SomePublisher();

            for (int i = 0; i < 10; i++)
            {
                SomeSubscriber subscriber = new SomeSubscriber(publisher);
                subscriber = null;
            }

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

            Console.WriteLine(SomeSubscriber.Count.ToString());


            Console.ReadLine();
        }
    }

    public class SomePublisher
    {
        public event EventHandler SomeEvent;
    }

    public class SomeSubscriber
    {
        public static int Count;

        public SomeSubscriber(SomePublisher publisher)
        {
            publisher.SomeEvent += new EventHandler(publisher_SomeEvent);
        }

        ~SomeSubscriber()
        {
            SomeSubscriber.Count++;
        }

        private void publisher_SomeEvent(object sender, EventArgs e)
        {
            // TODO: something
            string stub = "";
        }
    }

我建议,首先分析输出可能是什么,然后运行,然后阅读以下原因:

{仅在程序结束后才隐式调用析构函数。为了确定性地清除对象,必须实现IDisposable并显式调用Dispose()。这就是本质!:)


2

还有一件事,显式触发GC Collect可能不会提高程序的性能。很可能使情况变得更糟。

.NET GC经过精心设计和调整,可以自适应,这意味着它可以根据程序内存使用的“习惯”来调整GC0 / 1/2阈值。因此,经过一段时间的运行,它将适应您的程序。显式调用GC.Collect之后,阈值将被重置!.NET必须花费时间来再次适应程序的“习惯”。

我的建议是始终信任.NET GC。如果出现任何内存问题,请检查“ .NET内存”性能计数器并诊断我自己的代码。


6
我认为最好将此答案与以前的答案合并。
Salamander2007

1

不确定这是否是最佳做法...

建议:不确定时不要执行此操作或执行任何操作。在知道事实时重新评估,然后在性能测试之前/之后执行以进行验证。


0

但是,如果可以可靠地测试代码以确认调用Collect()不会产生负面影响,请继续...

恕我直言,这类似于说“如果您可以证明您的程序将来永远不会有任何错误,那就继续...”

严格地说,强制GC对于调试/测试目的很有用。如果您觉得需要在其他任何时间进行操作,则可能是您输入错误,或者您的程序构建错误。无论哪种方式,解决方案都不会强制GC ...


“那么,要么您弄错了,要么您的程序被错误地构建了。无论哪种方式,解决方案都不会强迫GC ...”绝对几乎总是不正确的。在某些特殊情况下,这是有道理的。
轧辊
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.