为什么C#垃圾收集行为对于Release和Debug可执行文件有所不同? [英] Why C# Garbage Collection behavior differs for Release and Debug executables?

查看:59
本文介绍了为什么C#垃圾收集行为对于Release和Debug可执行文件有所不同?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

Let's consider following simple program:

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

在发布模式下构建时,可以预期地打印:

When built in Release mode, it predictably prints:

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

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

When Debug version is launched (not under the Debugger, usual launch from Windows Explorer), the output differs:

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

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

Running under the debugger for both versions doesn't change the output.

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

I've discovered this strange difference during debugging of my custom collection that keeps weak references to objects.

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

Why garbage collector in debug executables does not collect objects that clearly are not referenced?

更新:

如果在ot中执行对象创建,情况会有所不同她的方法:

Situation differs if object creation is performed in other method:

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版本中输出相同的输出:

It outputs the same output in Release and Debug versions:

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


推荐答案

简短的答案是,GC不需要执行您所描述的任何操作。长的答案是,为了使您更轻松地进行调试,在调试配置下更悲观地工作是很常见的。

The short answer is that the GC isn't required to do anything like what you're describing. The long answer is that it's not uncommon for something to work more pessimistically under debug configuration, in order to allow you to debug more easily.

例如,在这种情况下,因为您已将 obj 声明为方法内部某个位置的局部变量,所以C#编译器可以合理地选择保留该实例的引用,以便诸如Locals窗口或Watch窗口之类的实用程序在Visual Studio中可以正常运行。

For example, in this case, because you declared obj as a local variable somewhere inside the method, the C# compiler can reasonably choose to retain references of that instance, so that utilities like the Locals window or the Watch windows in Visual Studio can function predictably.

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

Indeed, this is the IL of your code generated using the Debug configuration:

.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:

And this is the IL generated using the Release configuration:

.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
}

注意在调试版本中,如何将 TestClass 实例作为本地实例保留在整个实例中方法:

Notice how in the Debug build, the TestClass instance is retained as a local throughout the entire method:

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

在C#代码的嵌套作用域中声明该变量的事实是不相关的,因为IL代码没有等价物nt嵌套作用域的概念。因此,无论哪种方式,都将变量声明为整个方法的局部变量。

The fact that you declared that variable in a nested scope in the C# code is irrelevant, because the IL code doesn't have an equivalent notion of nested scopes. So, the variable is declared as a local of the entire method either way.

还要注意如何在C#代码中手动执行此更改(局部变量内联):

Also notice how if you manually perform this change in your C# code (local variable inlining):

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

然后,Debug版本的IL也跳过本地声明,匹配发布配置:

Then the IL of the Debug build skips the local declaration as well, matching the Release configuration:

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

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

And similarly, the Debug configuration output matches the output of the Release configuration as well:

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

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

Obviously, the reason for this is that part of the optimizations that the C# compiler performs when building using the Release configuration is to automatically inline local variables wherever possible. And that's where the different behavior kicks in.

这篇关于为什么C#垃圾收集行为对于Release和Debug可执行文件有所不同?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆