这里发生了什么? (.Net)GC.CollectionCount(0)不断增加

时间:2010-10-27 12:49:01

标签: c# .net mono garbage-collection memory-leaks

在测试应用程序性能时,我遇到了一些非常奇怪的GC行为。简而言之,GC甚至在没有运行时分配的空程序上运行!

以下应用程序演示了此问题:

using System;
using System.Collections.Generic;

public class Program
{
    // Preallocate strings to avoid runtime allocations.
    static readonly List<string> Integers = new List<string>();
    static int StartingCollections0, StartingCollections1, StartingCollections2;

    static Program()
    {
        for (int i = 0; i < 1000000; i++)
            Integers.Add(i.ToString());
        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();
    }

    static void Main(string[] args)
    {
        DateTime start = DateTime.Now;
        int i = 0;
        Console.WriteLine("Test 1");
        StartingCollections0 = GC.CollectionCount(0);
        StartingCollections1 = GC.CollectionCount(1);
        StartingCollections2 = GC.CollectionCount(2);
        while (true)
        {
            if (++i >= Integers.Count)
            {
                Console.WriteLine();
                break;
            }

            // 1st test - no collections!
            {
                if (i % 50000 == 0)
                {
                    PrintCollections();
                    Console.Write(" - ");
                    Console.WriteLine(Integers[i]);
                    //System.Threading.Thread.Sleep(100);
                    // or a busy wait (run in debug mode)
                    for (int j = 0; j < 50000000; j++)
                    { }
                }
            }
        }

        i = 0;
        Console.WriteLine("Test 2");
        StartingCollections0 = GC.CollectionCount(0);
        StartingCollections1 = GC.CollectionCount(1);
        StartingCollections2 = GC.CollectionCount(2);
        while (true)
        {
            if (++i >= Integers.Count)
            {
                Console.WriteLine("Press any key to continue...");
                Console.ReadKey(true);
                return;
            }

            DateTime now = DateTime.Now;
            TimeSpan span = now.Subtract(start);
            double seconds = span.TotalSeconds;

            // 2nd test - several collections
            if (seconds >= 0.1)
            {
                PrintCollections();
                Console.Write(" - ");
                Console.WriteLine(Integers[i]);
                start = now;
            }
        }
    }

    static void PrintCollections()
    {
        Console.Write(Integers[GC.CollectionCount(0) - StartingCollections0]);
        Console.Write("|");
        Console.Write(Integers[GC.CollectionCount(1) - StartingCollections1]);
        Console.Write("|");
        Console.Write(Integers[GC.CollectionCount(2) - StartingCollections2]);
    }
}

有人可以解释这里发生了什么吗?我的印象是,除非内存压力达到特定限制,否则GC不会运行。但是,它似乎一直在运行(并收集) - 这是正常的吗?

编辑:我修改了程序以避免所有运行时分配。

编辑2:好的,新的迭代,似乎DateTime是罪魁祸首。其中一个DateTime方法分配内存(可能是Subtract),这会导致GC运行。第一个测试现在导致绝对没有集合 - 正如预期的那样 - 而第二个导致几个集合。

简而言之,GC只在需要运行时运行 - 我只是在不知不觉中产生内存压力(DateTime是一个结构,我认为它不会产生垃圾)。

4 个答案:

答案 0 :(得分:5)

GC.CollectionCount(0)返回以下内容:

  

自进程启动以来指定代的垃圾收集发生的次数。

因此,您应该看到数字增加,并且这种增加并不意味着内存泄漏但GC已经运行。

同样在第一种情况下,您可以看到这种增加。它会发生得慢得多,因为非常慢的Console.WriteLine方法被更频繁地调用,减慢了很多东西。

答案 1 :(得分:3)

此处应该注意的另一件事是GC.Collect() 不是同步函数调用。它触发垃圾收集,但垃圾收集发生在后台线程上,理论上,当你到处检查GC统计信息时,它可能还没有完成运行。

GC.WaitForPendingFinalizers之后可以进行GC.Collect调用,直到垃圾收集发生为止。

如果您真的想在不同情况下尝试准确跟踪GC统计信息,我会在您的流程中使用Windows性能监视器,您可以在其中创建各种事物的监视器,包括.NET堆统计信息。

答案 2 :(得分:1)

如果您只是等待几秒钟,您会发现第一次测试中的收集计数也会增加,但速度不会很快。

代码之间的差异在于第一个测试会尽可能快地写出集合计数,而第二个测试在没有写入任何内容的情况下循环,直到达到时间限制。

第一个测试花费大部分时间等待文本写入控制台,而第二个测试花费大部分时间循环,等待时间限制。第二次测试将在同一时间内进行更多的迭代。

我计算了迭代次数,并打印出每次垃圾回收的迭代次数。在我的计算机上,第一次测试稳定了每个GC大约45000次迭代,而第二次测试稳定了每个GC大约130000次迭代。

所以,第一次测试实际上比第二次测试的更多垃圾收集,大约是第三次测试。

答案 3 :(得分:0)

谢谢大家!您的建议有助于揭示罪魁祸首:DateTime正在分配堆内存。

GC不会一直运行,只有在分配内存时才会运行。如果内存使用量持平,GC将永远不会运行,GC.CollectionCount(0)将始终按预期返回0

测试的最新版本展示了这种行为。第一次测试运行不分配任何堆内存(GC.CollectionCount(0)仍为0),而第二次测试运行以非显而易见的方式分配内存:通过DateTime.Subtract() - &gt; Timespan

现在,DateTimeTimespan都是值类型,这就是我发现这种行为令人惊讶的原因。不过,你有它:毕竟存在内存泄漏。