【问题标题】:Why C# Garbage Collection behavior differs for Release and Debug executables? [duplicate]为什么发布和调试可执行文件的 C# 垃圾收集行为不同? [复制]
【发布时间】:2016-05-26 13:32:46
【问题描述】:

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

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

当启动 Debug 版本时(不在 Debugger 下,通常从 Windows Explorer 启动),输出不同:

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

【问题讨论】:

  • 用于调试目的
  • 你想说debug版没有垃圾回收?它是否记录在某处?
  • 他并没有说没有调试器,只是说它的行为不同。
  • 你暗示 gc 需要在你想要的时候收集死的东西。不需要这样做。

标签: c# garbage-collection


【解决方案1】:

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

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

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

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

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

这个规则的唯一例外——你不能预测本地的生命周期的规则——是 GC keepalive,顾名思义,保持本地活着。 keepalive 机制是为那些您必须在特定时间跨度内保持本地活动以保持程序正确性的罕见情况而发明的。这通常只在非托管代码互操作场景中发挥作用。

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

【讨论】:

    【解决方案2】:

    简短的回答是,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 配置进行构建时执行的部分优化是尽可能自动内联局部变量。这就是不同行为发挥作用的地方。

    【讨论】:

    • 调用时不需要GC收集没有引用的对象,这实际上是不正确的。它应该。它在发布版本中这样做。该问题与调试和发布配置的差异有关。 Evk 给出了正确答案。
    • @CodeFuller 好的,它在哪里(在 C# 或 CLR 规范中)说它应该?
    • msdn.microsoft.com/en-us/library/xe0c2357(v=vs.110).aspx 使用此方法尝试回收所有无法访问的内存。所有对象,无论它们在内存中存在多长时间,都会被考虑收集。
    • @CodeFuller 好吧,没有人说该对象没有考虑在您的调试版本中收集。
    • @CodeFuller:规范说集合将尝试回收内存。它并没有说它回收所有可以收集的内存。此外,GC 不会对局部变量的生命周期或该变量将被视为集合根的时间做出任何 承诺。 GC 也没有对何时收集可终结对象做出任何承诺。 GC 是一种非确定性机制,您不能依赖它具有确定性行为。 它根本没有那种行为。
    猜你喜欢
    • 2015-04-06
    • 1970-01-01
    • 1970-01-01
    • 2020-03-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多