38

我已经开始查看项目中的一些代码,发现如下内容:

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

这些行通常出现在旨在提高效率的基本原理下破坏对象的方法上。我做了这样的评论:

  1. 在销毁每个对象时显式调用垃圾回收会降低性能,因为这样做没有考虑到 CLR 性能是否绝对必要。
  2. 仅当其他对象正在完成时,按该顺序调用这些指令会导致每个对象被销毁。因此,一个可以独立销毁的对象必须等待另一个对象的销毁,而没有真正的必要性。
  3. 它可以产生死锁(见:this question

1、2和3是真的吗?你能提供一些参考来支持你的答案吗?

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

4

6 回答 6

34

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

你所有的观点都是真实的。(它产生死锁;但这并不意味着它总是。)调用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(),这两个项目将在下一次机会时收集。 随着集合的写入,o2最终将出现在 Gen1 - 这意味着自动 Gen0 集合不会释放该内存。

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

编辑:刚刚测试了这种行为,有些讽刺的是:如果你有这样的方法:

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

...然后它将明确地不释放 someObject 的内存,即使在 RELEASE 模式下:它会将其提升到下一代 GC 中。

于 2012-09-04T14:42:16.320 回答
8

有一点可以很容易理解:每次运行 GC 会自动清理许多对象(比如 10000 个)。在每次销毁后调用它每次运行都会清理大约一个对象。

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

此外,在每个对象之后清理有什么好处?这怎么能比批处理更有效呢?

于 2012-09-04T14:41:24.837 回答
7

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

即使没有这种调用,锁定在 finaliser 中也比你这里的更糟糕。

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

到目前为止,在我的职业生涯中,我已经这样做了 2 次,也许 3 次。(或者如果你包括我做的那些,测量结果,然后再拿出来,可能大约 5 或 6 次 - 这是你应该在做完之后总是测量的东西)。

如果您在短时间内处理数百或数千兆内存,然后在很长一段时间内切换到较少密集使用的内存,这可能是一个巨大甚至至关重要的改进明确收集。这就是这里发生的事情吗?

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

于 2012-09-05T00:10:31.787 回答
6

在这里查看我的另一个答案:

GC.Collect 与否?

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

大约只有一次你想自己调用 GC.Collect() 是当你有垃圾收集器很难知道的关于你的程序的特定信息时。典型示例是一个长时间运行的程序,具有明显的繁忙和轻负载周期。您可能希望在繁忙周期之前的轻负载周期快结束时强制收集,以确保资源在繁忙周期中尽可能空闲。但即使在这里,您可能会发现通过重新思考您的应用程序是如何构建的(即计划任务会更好地工作吗?),您会做得更好。

于 2012-09-04T14:42:59.340 回答
6

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

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

我们的 Web 服务有一个内存缓存,大小为 5-5000 万个字符串(每个键/值对约 80-140 个字符,具体取决于配置),此外,对于每个客户端请求,我们将构建 2 个矩阵,一个是双精度,一个是布尔值,然后传递给另一个服务来完成工作。对于 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));
}

在使用 .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)");
        }
    }

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

[编辑] 在此之后,我们发现大型矩阵存在一些额外的问题。我们已经开始遇到沉重的内存压力并且应用程序突然无法分配数组,即使进程/服务器有足够的内存(24gb 可用)。经过深入调查,我们发现该进程的备用内存几乎是“正在使用的内存”的 100%(24gb 正在使用,24gb 备用,1gb 空闲)。当“空闲”内存达到 0 时,应用程序将暂停 10 多秒,而待机被重新分配为空闲,然后它可以开始响应请求。

根据我们的研究,这似乎是由于大对象堆的碎片造成的。

为了解决这个问题,我们采取了两种方法:

  1. We are going to change to jagged array vs multi-dimensional arrays. This will reduce the amount of continuous memory required, and ideally keep more of these arrays out of the Large Object Heap.
  2. We are going to implement the arrays using the ArrayPool class.
于 2017-11-06T19:28:47.963 回答
2

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

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

于 2016-08-24T21:21:32.317 回答