为什么发布和调试可执行文件的 C# 垃圾收集行为不同? [英] Why C# Garbage Collection behavior differs for Release and Debug executables?

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

问题描述

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

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 Explorer 启动),输出不同:

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?

更新:

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

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 窗口这样的实用程序或者 Visual Studio 中的 Watch 窗口可以正常运行.

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
}

注意在 Debug 构建中,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 代码没有嵌套作用域的等效概念.因此,无论哪种方式,该变量都被声明为整个方法的局部变量.

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 也会跳过本地声明,匹配 Release 配置:

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# 垃圾收集行为不同?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

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