考虑下面的代码:

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


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

最佳答案

由于正在使用调试器,您在这里绊倒了,得出了非常错误的结论。您需要以在用户计算机上运行的方式运行代码。首先使用Build + Configuration Manager切换到Release build,将左上角的“ Active solution configuration”组合更改为“ Release”。接下来,进入“工具+选项”,“调试”,“常规”,然后取消选中“抑制JIT优化”选项。

现在,再次运行您的程序并修改源代码。请注意,多余的括号完全没有作用。并注意将变量设置为null不会有什么区别。它将始终打印“ 1”。现在,它可以按照您希望和期望的方式工作。

剩下的工作就是解释为什么在运行Debug版本时其工作原理如此不同。这需要说明垃圾收集器如何发现局部变量,以及如何通过提供调试器来影响局部变量。

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

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

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

与该表相关的一种几乎不可思议的方法是GC.KeepAlive()。这是一种非常特殊的方法,它根本不会生成任何代码。它的唯一职责是修改该表。它延长了局部变量的生存期,从而防止了它存储的引用被垃圾回收。唯一需要使用它的方法是阻止GC过于急切地收集引用,这可能在将引用传递给非托管代码的互操作方案中发生。垃圾收集器无法看到此类代码正在使用的此类引用,因为它不是由抖动编译的,因此没有表明在哪里查找引用的表。将委托对象传递给非托管函数(例如EnumWindows())是何时需要使用GC.KeepAlive()的示例。

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

现在,这也解释了您之前看到的内容以及为什么提出该问题。因为GC.Collect调用无法收集引用,所以它显示“ 0”。该表表明该变量一直在GC.Collect()调用之后使用,一直到方法结束。通过附加调试器并运行Debug构建来强制这样说。

现在将变量设置为null确实有效,因为GC将检查该变量并且不再看到引用。但是请确保您不会陷入许多C#程序员陷入的陷阱,实际上编写该代码是没有意义的。无论在运行Release版本中的代码时是否存在该语句都没有关系。实际上,抖动优化器将删除该语句,因为它根本不起作用。因此,即使看起来有效果,也请不要编写这样的代码。



关于此主题的最后一点说明,这就是程序员遇到麻烦,他们编写小型程序来使用Office应用程序执行操作。调试器通常将它们放在错误的路径上,他们希望Office程序按需退出。适当的方法是调用GC.Collect()。但是他们会在调试应用程序时发现它不起作用,并通过调用Marshal.ReleaseComObject()将他们带入从未有过的土地。手动内存管理,它很少能正常工作,因为它们很容易忽略不可见的接口引用。 GC.Collect()实际上有效,只是在调试应用程序时不起作用。

关于c# - 了解.NET中的垃圾收集,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/28238202/

10-16 09:59