【问题标题】:What is going on here? (.Net) GC.CollectionCount(0) keeps increasing这里发生了什么? (.Net) GC.CollectionCount(0) 不断增加
【发布时间】:2011-05-01 07:22:09
【问题描述】:

在测试应用程序性能时,我遇到了一些非常奇怪的 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 运行。第一个测试现在导致绝对 no 集合 - 正如预期的那样 - 而第二个导致几个。

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

【问题讨论】:

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


    【解决方案1】:

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

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

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

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

    【讨论】:

    • 这是我最初的惊讶:为什么 GC 会运行而没有内存泄漏?似乎 DateTime 毕竟在泄漏内存,这触发了 GC 收集。
    【解决方案2】:

    这里应该注意的另一件事是GC.Collect() 不是同步函数调用。它会触发垃圾回收,但垃圾回收发生在后台线程上,理论上可能在您检查GC 统计信息时尚未完成运行。

    有一个GC.WaitForPendingFinalizers 调用,您可以在GC.Collect 之后进行阻塞,直到垃圾回收发生。

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

    【讨论】:

      【解决方案3】:

      如果您只等待几秒钟,您会看到收集计数在第一次测试中也增加了,但没有那么快。

      代码之间的区别在于,第一个测试始终尽可能快地写出收集计数,而第二个测试循环不写任何内容,直到达到时间限制。

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

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

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

      【讨论】:

      • 谢谢,这是引导我走向正确方向的答案:为什么第一种情况会这样?答案是它调用 DateTime.Subtract 的频率是第二种情况的三倍左右 - 因此内存泄漏是三倍!
      • @The Fiddler:其实不是内存泄漏,只是内存吞吐量。托管对象不会泄漏内存。
      • 其中“内存泄漏”读作“意外的内存分配”。这不是通常意义上的内存泄漏(因为内存已被回收),但该术语适用于在运行时不得导致 GC 的代码(例如,游戏是 gen-0 集合可能导致丢帧)。这里的问题是指这样的代码。
      【解决方案4】:

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

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

      最新的测试迭代展示了这种行为。第一次测试运行不分配任何堆内存(GC.CollectionCount(0) 仍然是0),而第二次以不明显的方式分配内存:通过DateTime.Subtract() -> Timespan

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

      【讨论】:

      • 不,那不是真的。 DateTime 和 TimeSpan 和 DateTime.Subtract 不会在堆上分配任何东西。此外,仅仅因为 GC 正在收集并不意味着存在内存“泄漏”(无论您如何定义泄漏)。 GC 将不确定地运行,如果没有要收集的东西,它就不会收集任何东西。因此,在这种情况下存在泄漏并且值类型以某种方式被扔到堆上是不正确的。
      • @CodingYoshi 如果不是 DateTime.Subtract() 导致分配,那是什么?
      • @davidklempfner 我从来没有说过没有分配,我说堆上没有分配。这里的分配将在堆栈上。这个答案和问题中有很多错误信息:内存泄漏并不意味着这个问题和答案将其定义为。
      • @CodingYoshi 我的意思是堆分配。如果 DateTime.Subtact() 没有导致堆分配,那么分配 OP 所指的堆内存是什么?
      猜你喜欢
      • 1970-01-01
      • 2021-08-04
      • 1970-01-01
      • 2013-11-03
      • 1970-01-01
      • 1970-01-01
      • 2010-12-14
      • 2017-11-14
      相关资源
      最近更新 更多