【问题标题】:Could someone explain the behaviour of the garbage collector?有人可以解释垃圾收集器的行为吗?
【发布时间】:2009-11-26 13:49:50
【问题描述】:

我正在研究 C# 中的垃圾收集器(或者更确切地说是 CLR?),试图更好地理解 C# 中的内存管理。

我制作了一个小示例程序,将三个较大的文件读入byte[] 缓冲区。我想看看,如果

  • 我实际上需要做任何事情来提高内存效率
  • 在当前迭代结束后将 byte[] 设置为 null 时会产生任何影响
  • 最后是否在通过GC.Collect() 强制垃圾收集时有帮助

免责声明:我使用 Windows 任务管理器测量了内存消耗并四舍五入。我试了几次,但总体还是一样。

这是我的简单示例程序:

static void Main(string[] args)
{
    Loop();
}

private static void Loop()
{
    var list = new List<string> 
    { 
        @"C:\Users\Public\Music\Sample Music\Amanda.wma",       // Size: 4.75 MB
        @"C:\Users\Public\Music\Sample Music\Despertar.wma",    // Size: 5.92 MB
        @"C:\Users\Public\Music\Sample Music\Distance.wma",     // Size: 6.31 MB
    };

    Console.WriteLine("before loop");
    Console.ReadLine();

    foreach (string pathname in list)
    {
        // ... code here ...

        Console.WriteLine("in loop");
        Console.ReadLine();
    }

    Console.WriteLine(GC.CollectionCount(1));
    Console.WriteLine("end loop");
    Console.ReadLine();
}

对于每个测试,我只更改了foreach 循环的内容。然后我运行程序,在每个Console.ReadLine() 我停下来检查Windows任务管理器中进程的内存使用情况。我记下了使用的内存,然后用 return 继续程序(我知道断点;))。就在循环结束后,我向控制台写了GC.CollectionCount(1),以查看GC 跳入的频率。


结果


测试 1:
foreach ( ... )
{
    byte[] buffer = File.ReadAllBytes(pathname);

    Console.WriteLine ...
}

结果(使用的内存):

before loop:   9.000 K 
1. iteration: 13.000 K
2. iteration: 19.000 K
3. iteration: 25.000 K
after loop:   25.000 K
GC.CollectionCount(1): 2

测试 2:

foreach ( ... )
{
    byte[] buffer = File.ReadAllBytes(pathname);
    buffer = null;

    Console.WriteLine ...
}

结果(使用的内存):

before loop:   9.000 K 
1. iteration: 13.000 K
2. iteration: 14.000 K
3. iteration: 15.000 K
after loop:   15.000 K
GC.CollectionCount(1): 2

测试 3:

foreach ( ... )
{
    byte[] buffer = File.ReadAllBytes(pathname);
    buffer = null;
    GC.Collect();

    Console.WriteLine ...
}

结果(使用的内存):

before loop:   9.000 K 
1. iteration:  8.500 K
2. iteration:  8.600 K
3. iteration:  8.600 K
after loop:    8.600 K
GC.CollectionCount(1): 3


我不明白的:

  • 在测试 1 中,内存随着每次迭代而增加。因此我猜想在循环结束时内存没有被释放。但是 GC 仍然说它收集了 2 次 (GC.CollectionCount)。怎么样?
  • 在测试 2 中,将 buffer 设置为 null 显然会有所帮助。内存比测试 2 低。但为什么 GC.CollectionCount 输出 2 而不是 3? 为什么内存使用没有测试 3 中那么低?
  • 测试 3 使用的内存最少。我会说是因为 1. 对内存的引用被删除(buffer 设置为null),因此当通过GC.Collect() 调用垃圾收集器时,它可以释放内存。看起来很清楚。

如果有更多经验的人可以对以上几点有所了解,那真的会对我有所帮助。很有趣的话题恕我直言。

【问题讨论】:

  • stackoverflow.com/questions/1090451/… 的或多或少重复。
  • 非常大的概念,我建议您阅读重复问题的答案中提供的链接。
  • 也许您可能还想尝试通过使用不同的 GC.Collect() 重载来指定 GCCollectionMode,详见 msdn 文章 Induced Collections (msdn.microsoft.com/en-us/library/bb384155.aspx)
  • 您是否检查过#1 和#2 之间生成的IL 的差异?它们都包含数组的.locals,还是只包含#1?
  • 确保您了解为什么查看任务管理器内存计数是完全错误的事情。如果你在看一个停车场,有趣的问题是里面有多少辆车,需要多长时间才能找到一个停车位,车辆靠得有多近,但你看到的是停车场的总面积,这汽车离开时不会变小。

标签: c# garbage-collection


【解决方案1】:

查看您将整个 WMA 文件读入数组的事实,我会说这些数组对象是在大对象堆中分配的。这是一个以更 malloc 类型的方式管理的单独堆(因为压缩垃圾回收在处理大型对象时效率不高)。

大对象堆中的空间是根据不同的规则收集的,它不计入主生成计数,这样即使测试 1 和 2 之间的收集数量也不会有差异尽管内存正在被重新使用(那里收集的所有内容都是 Array 对象,而不是底层字节)。在测试 3 中,您每次循环都强制收集 - 大对象堆包含在其中,因此进程的内存使用量不会增加。

【讨论】:

  • 有趣,我不知道大对象堆。 “malloc 类型的方式”究竟是什么?我从未使用过 C,所以我不知道 malloc 的行为方式。
  • "所有被收集的都是 Array 对象,而不是底层字节" - Array 对象和它们的字节之间没有分隔;作为优化的一种形式,它们被分配为单个内存块,即“基础字节”紧跟Array vtable 和其他内部结构。数组也是如此。
【解决方案2】:

TaskManager 不是最好的工具。使用CLR Profiler 或者对于一些简单的事情,使用WriteLine 来显示GC.GetTotalMemory()

GC 的主要目的是分配和释放大量小对象。如果你想研究它,写一些创建很多(小)字符串的东西。确保您知道“Generational GC”的含义。

您当前的实验是使用大对象堆 (LOH),它有一套完全不同的规则和关注点。

【讨论】:

    【解决方案3】:

    给你一个我觉得可能对你有用的链接。

    http://msdn.microsoft.com/en-us/magazine/ee309515.aspx

    -余乔

    【讨论】:

      【解决方案4】:

      您通过任务管理器查看的内存使用情况是针对该进程的。请记住,CLR 代表您的应用程序管理内存,因此您通常不会看到 GC 堆的使用情况直接反映在进程内存使用情况中。

      分配和释放内存不是免费的,因此显然 CLR 会尝试优化这一点以降低成本。因此,当从堆中收集对象时,您可能会也可能不会看到释放给操作系统的内存。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2013-08-10
        • 2019-02-01
        • 2010-10-10
        • 2023-03-30
        • 2019-11-08
        • 1970-01-01
        相关资源
        最近更新 更多