为什么C#垃圾收集行为对于Release和Debug可执行文件有所不同?

时间:2016-05-26 13:32:47

标签: c# garbage-collection

让我们考虑一下简单的程序:

class Program
{
    class TestClass
    {
        ~TestClass()
        {
            Console.WriteLine("~TestClass()");
        }
    }

    static void Main(string[] args)
    {
        WeakReference weakRef;
        {
            var obj = new TestClass();
            weakRef = new WeakReference(obj);
            Console.WriteLine("Leaving the block");
        }

        Console.WriteLine("GC.Collect()");
        GC.Collect();
        System.Threading.Thread.Sleep(1000);
        Console.WriteLine("weakRef.IsAlive == {0}", weakRef.IsAlive);

        Console.WriteLine("Leaving the program");
    }
}

在发布模式下构建时,可预测打印:

Leaving the block
GC.Collect()
~TestClass()
weakRef.IsAlive == False
Leaving the program

启动调试版本时(不在调试器下,通常从Windows资源管理器启动),输出不同:

Leaving the block
GC.Collect()
weakRef.IsAlive == True
Leaving the program
~TestClass()

在两个版本的调试器下运行并不会改变输出。

我在自定义集合的调试过程中发现了这种奇怪的差异,它保留了对对象的弱引用。

为什么调试可执行文件中的垃圾收集器不会收集明显未被引用的对象?

更新

如果使用其他方法执行对象创建,情况会有所不同:

class Program
{
    class TestClass
    {
        ~TestClass()
        {
            Console.WriteLine("~TestClass()");
        }
    }

    static WeakReference TestFunc()
    {
        var obj = new TestClass();
        WeakReference weakRef = new WeakReference(obj);
        Console.WriteLine("Leaving the block");

        return weakRef;
    }

    static void Main(string[] args)
    {
        var weakRef = TestFunc();

        Console.WriteLine("GC.Collect()");
        GC.Collect();
        System.Threading.Thread.Sleep(1000);
        Console.WriteLine("weakRef.IsAlive == {0}", weakRef.IsAlive);

        Console.WriteLine("Leaving the program");
    }
}

它在Release和Debug版本中输出相同的输出:

Leaving the block
GC.Collect()
~TestClass()
weakRef.IsAlive == False
Leaving the program

2 个答案:

答案 0 :(得分:14)

Theodoros Chatzigiannakis有一个很好的答案,但我想我可能会澄清几点。

首先,确实,C#编译器根据优化是打开还是关闭生成不同的代码。在优化关闭的情况下,本地生成在IL中。通过优化,可以制作一些当地人"短暂的&#34 ;;也就是说,编译器可以确定可以单独在评估堆栈上生成和使用本地的值,而无需为局部变量实际保留编号的槽。

这对抖动的影响是,作为编号的时隙生成的局部变量可以作为堆栈帧上的特定地址进行处理;这些变量被认为是垃圾收集器的根,并且当C#编译器认为它们超出范围时,它们通常不会被清零。因此,它们仍然是整个方法激活的根源,并且GC不会收集该根所引用的任何内容。

仅仅进入评估堆栈的值更可能是(1)被推入和弹出线程堆栈的短期值,或(2)已注册,并被快速覆盖。无论哪种方式,即使堆栈槽或寄存器是根,也会快速覆盖引用的值,因此收集器将不再认为它是可达的。

现在,抖动行为的描述暗示了一个重要的一点: C#编译器和抖动可以一起工作以随时延长或缩短局部变量的生命周期。此外,在C#规范中明确说明了这一事实。绝对不能依赖于具有任何特定行为的垃圾收集器一个地方的一生。

此规则的唯一例外 - 您无法预测本地生命周期的规则 - 就像名称所暗示的那样,GC keepalive将保持本地存活。 keepalive机制是针对那些罕见的情况发明的,在这些情况下,必须在特定的时间段内保持本地存活,以保持程序的正确性。这通常仅在非托管代码互操作方案中发挥作用。

再次,让我绝对清楚:调试和发布版本的行为是不同的,你应该达到的结论是"调试版本具有可预测的GC行为,发布版本没有"。你应该达到的结论是" GC行为未指明;变量的生命周期可以任意改变;在任何情况下我都不能依赖任何特定的GC行为"。 (除非之前提到过,否则keepalive会保持活力。)

答案 1 :(得分:10)

简短的回答是,GC不需要像您所描述的那样做任何事情。长期的答案是,在调试配置下更悲观地工作的情况并不少见,以便您可以更轻松地进行调试。

例如,在这种情况下,因为您在方法内部的某处将obj声明为局部变量,所以C#编译器可以合理地选择保留该实例的引用,以便像Locals窗口或Watch这样的实用程序Visual Studio中的窗口可以预测。

实际上,这是使用Debug配置生成的代码的IL:

.method private hidebysig static void Main (
        string[] args
    ) cil managed 
{
    .entrypoint
    .locals init (
        [0] class [mscorlib]System.WeakReference weakRef,
        [1] class _GC.Program/TestClass obj
    )

    IL_0000: nop
    IL_0001: nop
    IL_0002: newobj instance void _GC.Program/TestClass::.ctor()
    IL_0007: stloc.1
    IL_0008: ldloc.1
    IL_0009: newobj instance void [mscorlib]System.WeakReference::.ctor(object)
    IL_000e: stloc.0
    IL_000f: ldstr "Leaving the block"
    IL_0014: call void [mscorlib]System.Console::WriteLine(string)
    IL_0019: nop
    IL_001a: nop
    IL_001b: ldstr "GC.Collect()"
    IL_0020: call void [mscorlib]System.Console::WriteLine(string)
    IL_0025: nop
    IL_0026: call void [mscorlib]System.GC::Collect()
    IL_002b: nop
    IL_002c: ldc.i4 1000
    IL_0031: call void [mscorlib]System.Threading.Thread::Sleep(int32)
    IL_0036: nop
    IL_0037: ldstr "weakRef.IsAlive == {0}"
    IL_003c: ldloc.0
    IL_003d: callvirt instance bool [mscorlib]System.WeakReference::get_IsAlive()
    IL_0042: box [mscorlib]System.Boolean
    IL_0047: call void [mscorlib]System.Console::WriteLine(string,  object)
    IL_004c: nop
    IL_004d: ldstr "Leaving the program"
    IL_0052: call void [mscorlib]System.Console::WriteLine(string)
    IL_0057: nop
    IL_0058: ret
}

这是使用Release配置生成的IL:

.method private hidebysig static void Main (
        string[] args
    ) cil managed 
{
    .entrypoint
    .locals init (
        [0] class [mscorlib]System.WeakReference weakRef
    )

    IL_0000: newobj instance void _GC.Program/TestClass::.ctor()
    IL_0005: newobj instance void [mscorlib]System.WeakReference::.ctor(object)
    IL_000a: stloc.0
    IL_000b: ldstr "Leaving the block"
    IL_0010: call void [mscorlib]System.Console::WriteLine(string)
    IL_0015: ldstr "GC.Collect()"
    IL_001a: call void [mscorlib]System.Console::WriteLine(string)
    IL_001f: call void [mscorlib]System.GC::Collect()
    IL_0024: ldc.i4 1000
    IL_0029: call void [mscorlib]System.Threading.Thread::Sleep(int32)
    IL_002e: ldstr "weakRef.IsAlive == {0}"
    IL_0033: ldloc.0
    IL_0034: callvirt instance bool [mscorlib]System.WeakReference::get_IsAlive()
    IL_0039: box [mscorlib]System.Boolean
    IL_003e: call void [mscorlib]System.Console::WriteLine(string,  object)
    IL_0043: ldstr "Leaving the program"
    IL_0048: call void [mscorlib]System.Console::WriteLine(string)
    IL_004d: ret
}

请注意,在Debug构建中,TestClass实例在整个方法中保留为本地实例:

    .entrypoint
    .locals init (
        [0] class [mscorlib]System.WeakReference weakRef,
        [1] class _GC.Program/TestClass obj
    )

您在C#代码中的嵌套作用域中声明该变量的事实是无关紧要的,因为IL代码没有嵌套作用域的等效概念。因此,变量被声明为整个方法的局部。

另请注意如果在C#代码中手动执行此更改(本地变量内联):

        WeakReference weakRef;
        {
            weakRef = new WeakReference(new TestClass());
            Console.WriteLine("Leaving the block");
        }

然后,Debug构建的IL也会跳过本地声明,匹配Release配置:

.method private hidebysig static void Main (
        string[] args
    ) cil managed 
{
    .entrypoint
    .locals init (
        [0] class [mscorlib]System.WeakReference weakRef
    )

同样,Debug配置输出也匹配Release配置的输出:

Leaving the block
GC.Collect()
~TestClass()
weakRef.IsAlive == False
Leaving the program

显然,原因是C#编译器在使用Release配置构建时执行的部分优化是尽可能自动内联局部变量。这就是不同行为的所在。