C# 理解.NET中的垃圾收集

C# 理解.NET中的垃圾收集,c#,.net,garbage-collection,C#,.net,Garbage Collection,考虑以下代码: public class Class1 { public static int c; ~Class1() { c++; } } public class Class2 { public static void Main() { { var c1=new Class1(); //c1=null; // If this line is not commen

考虑以下代码:

public class Class1
{
    public static int c;
    ~Class1()
    {
        c++;
    }
}

public class Class2
{
    public static void Main()
    {
        {
            var c1=new Class1();
            //c1=null; // If this line is not commented out, at the Console.WriteLine call, it prints 1.
        }
        GC.Collect();
        GC.WaitForPendingFinalizers();
        Console.WriteLine(Class1.c); // prints 0
        Console.Read();
    }
}

现在,即使在调用
GC.Collect()
时,main方法中的变量
c1
超出了范围,并且没有被任何其他对象进一步引用,为什么它没有在那里最终确定?

您在这里被绊倒,并且得出了非常错误的结论,因为您正在使用调试器。您需要以代码在用户机器上运行的方式运行代码。使用build+Configuration manager首先切换到发布版本,将左上角的“活动解决方案配置”组合更改为“发布”。接下来,进入工具+选项、调试、常规并取消选中“抑制JIT优化”选项

现在再次运行程序并修改源代码。请注意,额外的大括号完全没有效果。请注意,将变量设置为null完全没有区别。它将始终打印“1”。它现在按照您希望和期望的方式工作

剩下的任务就是解释为什么在运行调试构建时它的工作方式如此不同。这需要解释垃圾收集器如何发现局部变量,以及调试器的存在如何影响局部变量

首先,抖动在将方法的IL编译为机器代码时执行两项重要任务。第一个在调试器中非常可见,您可以在“调试+窗口+反汇编”窗口中看到机器代码。然而,第二种责任是完全看不见的。它还生成一个表,描述如何使用方法体中的局部变量。该表中的每个方法参数和具有两个地址的局部变量都有一个条目。变量将首先存储对象引用的地址。以及不再使用该变量的机器代码指令的地址。此外,该变量是否存储在堆栈帧或cpu寄存器上

此表对于垃圾收集器是必不可少的,它需要知道在执行收集时在何处查找对象引用。当引用是GC堆上对象的一部分时,很容易做到。当对象引用存储在CPU寄存器中时,显然不容易做到。桌子上写着去哪里看

表中的“不再使用”地址非常重要。它使垃圾收集器非常高效。它可以收集一个对象引用,即使它在一个方法中使用,而该方法还没有完成执行。这是很常见的,例如Main()方法只会在程序终止之前停止执行。显然,您不希望在Main()方法中使用的任何对象引用在程序运行期间有效,这将导致泄漏。jitter可以使用表来发现这样一个局部变量不再有用,这取决于程序在调用Main()方法之前在该方法中的进度

与该表相关的一个几乎神奇的方法是GC.KeepAlive()。这是一个非常特殊的方法,它根本不生成任何代码。它唯一的职责就是修改那张表。它延长了局部变量的生存期,防止它存储的引用被垃圾回收。您需要使用它的唯一时间是阻止GC过于急切地收集引用,这可能发生在互操作场景中,其中引用被传递给非托管代码。垃圾收集器无法看到这样的代码正在使用这样的引用,因为它不是由jitter编译的,所以没有说明在何处查找引用的表。将委托对象传递给EnumWindows()等非托管函数是需要使用GC.KeepAlive()的样板示例

因此,正如您在发布版本中运行示例代码段后可以从中看出的那样,可以在方法完成执行之前提前收集局部变量。更强大的是,如果某个对象的某个方法不再引用该对象,则可以在该对象的某个方法运行时收集该对象。这有一个问题,调试这样的方法是非常尴尬的。因为你可以把变量放在观察窗口或者检查它。如果发生GC,它将在调试时消失。这将是非常不愉快的,因此抖动意识到有一个调试器连接。然后修改表并更改“上次使用”的地址。并将其从正常值更改为方法中最后一条指令的地址。只要方法没有返回,它就可以保持变量的活动状态。它允许您一直监视它,直到方法返回为止

这也解释了你之前看到的以及你为什么问这个问题。它打印“0”,因为GC.Collect调用无法收集引用。该表显示,该变量一直在使用中,直到方法结束为止,都是通过GC.Collect()调用使用的。通过连接调试器并运行调试生成,强制这样说

将变量设置为null现在确实有效,因为GC将检查变量,并且不再看到引用。但要确保你不会落入许多C#程序员所落入的陷阱,实际上编写代码是毫无意义的。在发布版本中运行代码时,该语句是否存在没有任何区别。事实上,抖动优化器将删除该语句,因为它没有任何效果。因此,请确保不要编写这样的代码,即使它似乎有效果


关于这个主题的最后一个注意事项是,这会让编写小程序来使用Office应用程序的程序员陷入麻烦。调试器通常使他们走错了路,他们希望Office程序按需退出。正确的方法是调用GC.Collect()。但当他们调试自己的应用程序时,他们会发现它不起作用,导致他们进入neve