小编典典

了解 .NET 中的垃圾收集

all

考虑下面的代码:

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

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


阅读 102

收藏
2022-07-17

共1个答案

小编典典

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

现在再次运行您的程序并修改源代码。注意额外的大括号根本没有效果。并注意将变量设置为 null
没有任何区别。它总是打印“1”。它现在按您希望和预期的方式工作。

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

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

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

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

与该表相关的一个几乎神奇的方法是 GC.KeepAlive()。这是一种 非常 特殊的方法,它根本不会生成任何代码。它的唯一职责是修改该表。它 延伸
局部变量的生命周期,防止它存储的引用被垃圾收集。您需要使用它的唯一时间是阻止 GC
过度收集引用,这可能发生在将引用传递给非托管代码的互操作场景中。垃圾收集器看不到此类代码正在使用此类引用,因为它不是由抖动编译的,因此没有说明在哪里查找引用的表格。将委托对象传递给像
EnumWindows() 这样的非托管函数是需要使用 GC.KeepAlive() 时的样板示例。

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

现在这也解释了你之前看到的内容以及你问这个问题的原因。它打印“0”,因为 GC.Collect 调用无法收集引用。该表表明该变量在 GC.Collect()
调用之后一直在使用 一直到方法结束。 通过附加调试器并 运行调试版本来强制这么说。

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


关于这个主题的最后一点说明,这就是让编写小程序来使用 Office 应用程序做某事的程序员陷入困境的原因。调试器通常会让他们走上错误的道路,他们希望
Office 程序按需退出。适当的方法是调用 GC.Collect()。但是当他们调试他们的应用程序时,他们会发现它不起作用,通过调用
Marshal.ReleaseComObject()
将他们引导到永远不会到达的地方。手动内存管理,它很少正常工作,因为它们很容易忽略不可见的接口引用。GC.Collect()
确实有效,只是在调试应用程序时无效。

2022-07-17