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

Cod*_*ler 5 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");
    }
}
Run Code Online (Sandbox Code Playgroud)

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

Leaving the block
GC.Collect()
~TestClass()
weakRef.IsAlive == False
Leaving the program
Run Code Online (Sandbox Code Playgroud)

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

Leaving the block
GC.Collect()
weakRef.IsAlive == True
Leaving the program
~TestClass()
Run Code Online (Sandbox Code Playgroud)

在两个版本的调试器下运行不会更改输出.

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

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

更新:

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

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");
    }
}
Run Code Online (Sandbox Code Playgroud)

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

Leaving the block
GC.Collect()
~TestClass()
weakRef.IsAlive == False
Leaving the program
Run Code Online (Sandbox Code Playgroud)

Eri*_*ert 14

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

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

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

其价值仅仅走上计算堆栈是更可能是:(1)推入和弹出线程的堆栈,或者(2)enregistered,并迅速覆盖短期值.无论哪种方式,即使堆栈槽或寄存器是根,也会快速覆盖引用的值,因此收集器将不再认为它是可达的.

:现在,很重要的一点是通过抖动行为的描述暗示C#编译器和抖动可以一起延长或缩短一个局部变量的生命周期,在他们的心血来潮任何时候.而且,这一事实在C#规范中有明确规定.绝对不能依赖垃圾收集器在本地生命周期中有任何特殊行为.

唯一的例外-这个规则,你就不会去当地的寿命预测-是一个GC保活会,顾名思义,保留本地活着.keepalive机制是为少数情况发明的,在这种情况下,为了保持程序的正确性,你必须让当地人保持特定的时间.这通常仅在非托管代码互操作方案中发挥作用.

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


The*_*kis 10

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

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

实际上,这是使用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
}
Run Code Online (Sandbox Code Playgroud)

这是使用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
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

        WeakReference weakRef;
        {
            weakRef = new WeakReference(new TestClass());
            Console.WriteLine("Leaving the block");
        }
Run Code Online (Sandbox Code Playgroud)

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

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

类似地,Debug配置输出也匹配Release配置的输出:

Leaving the block
GC.Collect()
~TestClass()
weakRef.IsAlive == False
Leaving the program
Run Code Online (Sandbox Code Playgroud)

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

  • @CodeFuller:规范说该集合将*尝试*回收内存.它并没有说它*将*回收*所有可能收集的内存*.此外,GC不会对局部变量的生命周期做出任何*承诺,也不会将该变量视为集合的根目录.GC也没有对何时收集可终结对象做出任何承诺.GC是一种非确定性机制,您不能依赖它具有确定性行为.**它根本没有这种行为.** (5认同)