为什么最后一个对象没有被垃圾收集器破坏?

时间:2019-10-30 22:09:43

标签: c# garbage-collection enumeration visual-studio-2019

请耐心等待我在这里使用枚举器和LINQ做“有趣的事情”。在检查是否可以正确清理所有东西的同时,我注意到我在创建的用于测试一些东西的简单控制台应用程序中发现了一些东西。但是首先,从通用的DoAll()方法开始,做一些代码:

public static IEnumerable<T> DoAll<T>(this IEnumerable<T> data, Action<T> action)
{
    foreach (var item in data)
    {
        action(item);
        yield return item;
    }
}

这只是对项目执行特定的操作,然后将其交给管道中的下一个方法。接下来,一个仅作为示例的基本IDisposable对象:

public class Dummy : IDisposable
{
    private static int _count = 0;
    public int Count = ++_count;
    public Dummy() => Console.WriteLine($"Dummy {Count} created.");
    ~Dummy() => Console.WriteLine($"Dummy {Count} destroyed.");
    public void Dispose() => Console.WriteLine($"Dummy {Count} disposed.");
    private int _value = 0;
    public int Value => ++_value;
}

虚拟对象只是一个对象,它统计一个对象被创建的频率以及一个跟踪其被调用频率的值。如我所说,简单的例子。现在我需要一个枚举器!这个:

public static class DummyList
{
    public static IEnumerable<int> GetDummy()
    {
        using (var dummy = new Dummy())
        {
            while (true) { yield return dummy.Value; }
        }
    }
}

这里没有火箭科学。只是带有静态扩展方法的静态类,它将在循环中永远调用Dummy Value方法,从而将每个值生成管道中的下一个方法。
需要明确的是,尽管这似乎是一个无限循环,但该循环将在下一个方法中中断。接下来的方法是测试方法:

public static void DummyTest()
    {
        for (var x = 0; x < 5; x++)
        {
            Console.WriteLine(
                string.Join(", ", 
                    DummyList.GetDummy()
                             .DoAll(i => Console.Write($"Got {i}! "))
                             .Take(10)
                             .Select(i => i.ToString())));
        }
        GC.Collect();
    }

好吧,上面的代码将使用我的枚举器5次,每次仅获取10个值,然后将其作为列表写入控制台。最后,我打电话给垃圾收集器进行清理。我只是从控制台应用程序中的main方法调用DummyTest();来获得以下结果:

Dummy 1 created.
Got 1! Got 2! Got 3! Got 4! Got 5! Got 6! Got 7! Got 8! Got 9! Got 10! Dummy 1 disposed.
1, 2, 3, 4, 5, 6, 7, 8, 9, 10
Dummy 2 created.
Got 1! Got 2! Got 3! Got 4! Got 5! Got 6! Got 7! Got 8! Got 9! Got 10! Dummy 2 disposed.
1, 2, 3, 4, 5, 6, 7, 8, 9, 10
Dummy 3 created.
Got 1! Got 2! Got 3! Got 4! Got 5! Got 6! Got 7! Got 8! Got 9! Got 10! Dummy 3 disposed.
1, 2, 3, 4, 5, 6, 7, 8, 9, 10
Dummy 4 created.
Got 1! Got 2! Got 3! Got 4! Got 5! Got 6! Got 7! Got 8! Got 9! Got 10! Dummy 4 disposed.
1, 2, 3, 4, 5, 6, 7, 8, 9, 10
Dummy 5 created.
Got 1! Got 2! Got 3! Got 4! Got 5! Got 6! Got 7! Got 8! Got 9! Got 10! Dummy 5 disposed.
1, 2, 3, 4, 5, 6, 7, 8, 9, 10
Dummy 4 destroyed.
Dummy 3 destroyed.
Dummy 2 destroyed.
Dummy 1 destroyed.

嗯,看起来几乎还可以。每次创建和处置对象时,即使我在循环中将其断开,我也知道我的枚举将被清理。我想确保这会发生,我首先想在这里问是否会发生。但是此测试表明确实如此。
但是,垃圾收集器只会破坏5个对象中的4个!这让我有些麻烦。所有显示我的枚举器的代码都将很好地清理,只是发现它不会清理所有内容。并且尽管它并不那么重要,但我确实想知道为什么它不会删除最后一个对象,以及在这个复杂的示例中如何迫使垃圾收集器销毁它们。

那为什么不破坏最后一个对象?如何在DummyTest方法中强制它破坏?


事情变得越来越复杂。此错误仅在“调试”模式下发生,而不在“发布”模式下发生。当它跳过Dummy 5时,我没有对其进行调试,但是我已经编译了带有和不带有调试信息的代码。没有调试信息,虚拟5将被释放。当使用调试信息编译项目时,该虚拟对象似乎保留了某些东西。
我想知道为什么,以及如何防止这种情况。

1 个答案:

答案 0 :(得分:0)

似乎原因与垃圾收集器有关,垃圾收集器在调试模式下的行为与在释放模式下的行为不同。正如某些人评论的那样,当DummyTest()方法关闭时,垃圾编译器可能仍保留对Dummy 5的引用,因为它将保留该值以用于调试。在释放模式下,垃圾收集器的行为更加激进。
这个问题与Why C# Garbage Collection behavior differs for Release and Debug executables?有相似之处,并在Does garbage collection run during debug?中得到了回答,但是这个问题仍然很有趣,因为它适用于资源是更复杂的无限枚举的一部分的情况。
但是,这不是内存泄漏!只是当在调试模式下编译项目时,垃圾收集器往往会在某些资源上停留更长的时间,直到关闭该方法之后为止,因为任何正在运行的调试器可能仍在评估该值。 (它不知道没有调试器在运行。)