有人能解释一下垃圾回收器的行为吗?

6

我正在尝试使用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

Test 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仍然表示它已经收集了两次(GC.CollectionCount)。为什么?
  • 在测试2中,显然设置buffernull有所帮助。内存比测试1低。但为什么GC.CollectionCount输出2而不是3?为什么内存使用量不如测试3低?
  • 测试3使用的内存最少。我认为这是因为1.对内存的引用被删除(将buffer设置为null),因此当通过GC.Collect()调用垃圾回收器时,它可以释放内存。似乎非常清楚。

如果有更多经验的人能够解决上述一些问题,那真的会对我很有帮助。我认为这是一个非常有趣的话题。


1
基本上是 https://dev59.com/TUjSa4cB1Zd3GeqPEE_2 的副本。 - Binary Worrier
非常重要的概念,我建议您阅读在重复问题的答案中提供的链接。 - Binary Worrier
也许您还想尝试使用GC.Collect()的不同重载来指定GCCollectionMode,详见Induced Collections(http://msdn.microsoft.com/en-us/library/bb384155.aspx)的msdn文章。 - Daniel Robinson
你有检查过 #1 和 #2 生成的 IL 代码之间的差异吗?它们两个都包含数组的 .locals,还是只有 #1 包含? - Pavel Minaev
确保你明白为什么观察任务管理器的内存计数完全是错误的事情。如果你在看一个停车场,有趣的问题是有多少辆车在里面,需要多长时间才能找到一个停车位,车子之间有多近,但你正在观察停车场的总大小,而这个大小并不会因为车子离开而变小。 - Eric Lippert
4个回答

6
看到您正在将整个WMA文件读入数组中,我会说这些数组对象被分配在大对象堆中。这是一个单独的堆,以一种更类似于malloc的方式进行管理(因为紧凑垃圾收集不能有效地处理大型对象)。
大对象堆中的空间根据不同的规则进行收集,并且它不计入主要代数计数,这就是为什么您在测试1和2之间没有看到收集次数的差异,即使内存被重复使用(那里收集的只是数组对象,而不是底层字节)。 在测试3中,您每次循环都强制进行收集 - 大对象堆也包括在其中,因此进程的内存使用情况不会增加。

有趣,我不知道大对象堆。 “malloc-type way”是什么?我从未使用过C,因此我不知道malloc的行为。 - Max
那里收集的只是Array对象,而不是底层字节。在Array对象和它们的字节之间没有分离;它们被分配为单个内存块作为一种优化形式,即“底层字节”紧随Array vtable和其他内部结构之后。数组也是如此。 - Pavel Minaev

2
TaskManager不是最好的工具。使用CLR Profiler或者对于简单的事情,使用WriteLine来显示GC.GetTotalMemory()
GC的主要目的是分配和释放大量小对象。如果你想研究它,写一些创建许多(较小的)字符串等的东西。确保你知道“分代GC”的含义。
你当前的实验正在操作大对象堆(LOH),它有一整套其他规则和关注点。

1

0

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

分配和释放内存并非免费,因此CLR将尝试优化以减少成本。因此,当从堆中收集对象时,您可能会看到内存是否释放给操作系统。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接