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

JPC*_*PCF 31 .net c# garbage-collection

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

GC.Collect();
GC.WaitForPendingFinalizers();
Run Code Online (Sandbox Code Playgroud)

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

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

1,2和3都是真的吗?你能给出一些支持你答案的参考吗?

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

Dan*_*zey 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();
    }
    
    Run Code Online (Sandbox Code Playgroud)

没有a GC.Collect(),这两个项目将在下次机会时收集. 随着集合为writte,o2将最终在Gen1中 - 这意味着自动Gen0集合将不会释放该内存.

值得注意的还有一个更大的恐怖:在DEBUG模式下,GC的功能不同,并且不会回收任何仍在范围内的变量(即使它在当前方法中未被使用).所以在调试模式下,上面的代码甚至不会收集o1打电话时GC.Collect,所以两者o1o2会得到提升.调试代码时,这可能会导致一些非常不稳定和意外的内存使用.(文章如这个亮点这种行为.)

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

void CleanUp(Thing someObject)
{
    someObject.TidyUp();
    someObject = null;
    GC.Collect();
    GC.WaitForPendingFinalizers(); 
}
Run Code Online (Sandbox Code Playgroud)

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

  • 最后的例子没有收集是有道理的。对 null 的赋值没有任何作用,因此应该将其取出,并且由于没有其他活动使用堆栈,因此编译器可以在本地范围内使用属于本地的空间这一事实无关紧要 - 它可以,但它没有理由这样做。 (3认同)
  • 在我目前的项目中,我们使用大型双精度数组(通常大小为10k),它们直接进入大对象堆.根据我的经验,.Net垃圾收集器(至少v.4)完全无法管理这些东西,所以我不得不添加`GC.Collect()`以使用2GB内存和交换内容来停止进程磁盘一直都是. (2认同)
  • 然后,即使在 RELEASE 模式下,它也不会明确释放 someObject 的内存:它会将其提升到下一个 GC 代。” - 尝试在 https://github.com/dotnet/coreclr/ 上测试类似的东西issues/15207加上非常丰富的信息https://github.com/dotnet/coreclr/issues/5826#issuecomment-226574611 (2认同)

usr*_*usr 8

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

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

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

  • 非常好的一点是,你将*每个*对象的成本命中,而不是将其分散到许多对象上. (4认同)

Jon*_*nna 6

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

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

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

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

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

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


Joe*_*orn 5

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

去GC.Collect与否?

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

大约是您唯一一次想要自己调用GC.Collect()的时间,是因为您拥有程序的特定信息,而这些信息对于垃圾收集器是很难知道的。典型的例子是一个长期运行的程序,具有明显的繁忙和轻负载周期。您可能想要在繁忙周期之前的轻负载周期即将结束时强制执行收集,以确保在繁忙周期中资源尽可能空闲。但是即使在这里,您也可以通过重新考虑应用程序的构建方式来发现自己做得更好(即,计划任务能否更好地工作?)。


Jus*_*tin 5

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

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

我们的网络服务有一个内存缓存,大约有 5-5000 万个字符串(每个键/值对约 80-140 个字符,具体取决于配置),此外,对于每个客户端请求,我们将构造 2 个矩阵,一个是 double,一个是布尔值然后传递给另一个服务来完成工作。对于 1000x1000“矩阵”(二维数组),每个请求大约为 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));
}
Run Code Online (Sandbox Code Playgroud)

在使用 .net 4.6 重写后,我们将垃圾收集分为 2 个步骤 - 一个简单的收集和一个压缩收集。

    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)");
        }
    }
Run Code Online (Sandbox Code Playgroud)

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