了解.NET 中的垃圾收集

考虑下面的代码:

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()时没有被任何其他对象进一步引用,为什么它没有在那里完成?

61249 次浏览

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

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

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

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

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

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

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

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

这也解释了您之前看到的情况以及为什么要问这个问题。它输出“0”,因为 GC.Collection 调用无法收集引用。该表表示变量在使用 过去的 GC.Collect ()调用中,一直到方法的末尾。通过让调试器通过运行调试版本附加 还有来强制说明这一点。

将变量设置为 null 现在确实有效果,因为 GC 将检查变量,不再看到引用。但是请确保您不会落入许多 C # 程序员已经落入的陷阱,实际上编写那些代码是毫无意义的。在发布版本中运行代码时,该语句是否存在没有任何区别。事实上,抖动优化器将 拿开该语句,因为它没有任何影响。因此,一定不要编写这样的代码,即使它 看起来有一个效果。


关于这个主题的最后一个注意事项是,编写小程序来使用 Office 应用程序做一些事情的程序员会遇到麻烦。调试器通常会让它们走上错误的路径,它们希望 Office 程序按需退出。实现这一点的适当方法是调用 GC.Collect ()。但是当他们调试他们的应用程序时,他们会发现它不工作,通过呼叫元帅把他们带到永远不会到达的地方。ReleaseComObject ().手动内存管理,它很少正常工作,因为他们很容易忽略一个不可见的接口引用。Collect ()实际上可以工作,只是在调试应用程序时不能工作。

[只是想进一步补充内部定稿过程]

您创建了一个对象,当对象被垃圾收集时,应该调用该对象的 Finalize方法。但是最终定稿不仅仅是这个简单的假设。

概念:

  1. 没有实现 Finalize方法的对象: 它们的内存被立即回收,当然,除非应用程序代码不再能够访问它们。

  2. 实现 Finalize方法的对象: 需要理解 Application RootsFinalization QueueFreachable Queue的概念,因为它们参与了回收过程。

  3. 如果应用程序代码无法访问任何对象,则将其视为垃圾。

假设: 类/对象 A、 B、 D、 G、 H 不实现 Finalize方法,而 C、 E、 F、 I、 J 实现 Finalize方法。

当应用程序创建新对象时,new操作符将从堆中分配内存。如果对象的类型包含 Finalize方法,则在终结队列上放置一个指向该对象的指针。因此,指向对象 C、 E、 F、 I、 J 的指针被添加到终结队列中。

终结队列终结队列终结队列是由垃圾收集器控制的内部数据结构。队列中的每个条目都指向一个对象,在回收该对象的内存之前,应该调用该对象的 Finalize方法。

下图显示了一个包含多个对象的堆。其中一些对象可以从 应用根访问,而另一些则不能。当创建对象 C、 E、 F、 I 和 J 时,。NET 框架检测到这些对象具有 Finalize方法,并将指向这些对象的指针添加到 终结队列终结队列终结队列

enter image description here

当发生 GC (第一个集合)时,对象 B、 E、 G、 H、 I 和 J 被确定为垃圾。A、 C、 D、 F 仍然可以通过上面黄色框中箭头所描述的应用程序代码访问。

垃圾收集器扫描终结队列,寻找指向这些对象的指针。当找到一个指针时,指针将从终结队列中移除,并追加到 可放行队列可放行队列(“ F- 可到达”,即终结器可到达)。可访问队列是由垃圾收集器控制的另一个内部数据结构。可访问队列中的每个指针标识一个对象,该对象已准备好调用其 Finalize方法。

在第一次 GC 之后,托管堆看起来与下图类似:

  1. 对象 B、 G 和 H 所占用的内存已被立即回收,因为这些对象没有需要调用的 finalize 方法。

  2. 然而,由于对象 E、 I 和 J 的 Finalize方法尚未被调用,因此无法回收它们所占用的内存。调用 Finalize 方法是由可连接队列完成的。

  3. < p > A,C,D,F 仍然可以通过上面黄色框中的箭头所描述的应用程序代码访问,因此它们在任何情况下都不会被收集。

enter image description here

有一个专门用于调用 Finalize 方法的特殊运行时线程。当可访问队列为空(通常是这种情况)时,此线程处于睡眠状态。但是,当条目出现时,该线程将唤醒,从队列中删除每个条目,并调用每个对象的 Finalize 方法。垃圾收集器压缩可回收内存,特殊的运行时线程清空可访问队列,执行每个对象的 Finalize方法。这里是 Finalize 方法执行的最后时刻。

下一次调用垃圾收集器(第二个 GC)时,它会发现最终的对象是真正的垃圾,因为应用程序的根不再指向它,而且可访问队列也不再指向它(它也是 EMPTY) ,因此对象 E、 I、 J 的内存可能会从堆中回收。见下图,并与上图进行比较。

enter image description here

这里需要理解的重要一点是,需要两个 GC 来回收需要终止的对象所使用的内存。实际上,甚至需要两个以上的集合,因为这些对象可能被提升到较老的一代。

注意: 可连接的队列被认为是根,就像全局变量和静态变量是根一样。因此,如果一个对象位于可访问队列上,那么该对象是可访问的,而不是垃圾。

最后,请记住,调试应用程序是一回事,垃圾收集是另一回事,它们的工作方式不同。到目前为止,仅仅通过调试应用程序是无法感受到垃圾收集的。如果您希望进一步研究内存,请启动 给你