一尘不染

了解.NET中的垃圾收集

c#

考虑下面的代码:

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()调用时没有被其他任何对象进一步引用,但为什么还没有在其中最终确定呢?


阅读 269

收藏
2020-05-19

共1个答案

一尘不染

由于正在使用调试器,您在这里绊倒了,得出了非常错误的结论。您需要以在用户计算机上运行的方式运行代码。首先使用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版本中运行示例片段后 看到的那样,可以在方法完成执行之前及早收集局部变量。更强大的是,如果对象的方法之一不再引用
this
,则该对象可以在其方法之一运行时被收集。这样做有一个问题,调试这种方法很尴尬。因为您可以将变量放入“监视”窗口中或进行检查。如果发生GC,在调试时它将
消失 。那将是非常不愉快的,因此抖动会 意识到 已附加调试器。然后 修改
表格并更改“上次使用”地址。并将其从其正常值更改为该方法中最后一条指令的地址。只要方法没有返回,它就使变量保持活动状态。这样您就可以继续观察它,直到方法返回为止。

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

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


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

2020-05-19