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

让我们考虑以下简单程序:

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"); } } 

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

 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 

简短的回答是,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 } 

这是使用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配置构建时执行的部分优化是尽可能自动内联局部变量。 这就是不同行为的起点。

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

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

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

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

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

这个规则的唯一例外 – 你不能对本地生命周期做出预测的规则 – 就像顾名思义一样,GC keepalive将保持本地存活。 keepalive机制是为少数情况发明的,在这种情况下,为了保持程序的正确性,您必须在特定的时间段内保持本地存活。 这通常仅在非托管代码互操作方案中发挥作用。

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