了解.NET中的垃圾收集

Vic*_*jee 161 .net c# 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 commented out, at the Console.WriteLine call, it prints 1.
        }
        GC.Collect();
        GC.WaitForPendingFinalizers();
        Console.WriteLine(Class1.c); // prints 0
        Console.Read();
    }
}
Run Code Online (Sandbox Code Playgroud)

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

Han*_*ant 340

你正在被绊倒并得出非常错误的结论,因为你正在使用调试器.您需要按照在用户计算机上运行的方式运行代码.首先使用Build + Configuration manager切换到Release版本,将左上角的"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版本中运行它之后可以从示例代码段中看到的那样,可以在方法执行完毕之前尽早收集局部变量.更为强大的是,虽然它的方法运行,如果该方法不再是指一个对象可以得到收集.有一个问题,调试这样的方法是非常尴尬的.因为您可以将变量放在Watch窗口中或检查它.如果发生GC ,它会在您调试时消失.这将是非常不愉快的,因此抖动意识到附加了调试器.然后它修改该表并改变"最后使用"的地址.并将其从正常值更改为方法中最后一条指令的地址.只要方法没有返回,它就会使变量保持活动状态.这允许您继续观察它直到方法返回.

现在,这也解释了您之前看到的内容以及您提出问题的原因.它打印"0",因为GC.Collect调用无法收集引用.该表说,该变量在使用过去的GC.Collect的()调用,一路攀升到方法的结束.通过附加调试器运行Debug构建强制这样说.

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


关于这个主题的最后一个注意事项,这就是程序员遇到麻烦,编写小程序来处理Office应用程序.调试器通常会将它们放在错误的路径上,他们希望Office程序按需退出.适当的方法是调用GC.Collect().但他们会发现,当他们调试他们的应用程序时它不起作用,通过调用Marshal.ReleaseComObject()将它们引入永不落伍的地方.手动内存管理,它很少正常工作,因为它们很容易忽略不可见的接口引用.GC.Collect()实际上是有效的,而不是在你调试应用程序时.


R.C*_*R.C 31

[只是想进一步补充最终化进程的内部]

因此,您创建一个对象,并在收集对象时,Finalize应调用该对象的方法.但是最终确定比这个非常简单的假设更多.

简短概念::

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

  2. 对象实现Finalize方法,体现/实现Application Roots,Finalization Queue,Freacheable Queue谈到他们可以被回收之前.

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

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

当应用程序创建新对象时,new运算符将从堆中分配内存.如果对象的类型包含Finalize方法,则指向该对象的指针将放置在终结队列中.

因此,指向对象C,E,F,I,J的指针被添加到终结队列中.

结束队列是由垃圾收集器控制的内部数据结构.队列中的每个条目都指向一个对象,该对象应该Finalize在回收对象的内存之前调用其方法.下图显示了包含多个对象的堆.其中一些对象可以从应用程序的根源访问,而有些则不是.当创建对象C,E,F,I和J时,.Net框架检测到这些对象具有Finalize方法,并且指向这些对象的指针被添加到终结队列中.

在此输入图像描述

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

垃圾收集器扫描终结队列以查找指向这些对象的指针.找到指针后,指针将从终结队列中删除并附加到可释放队列("F-reachable").

释放队列是由垃圾收集器控制的另一个内部数据结构.可分离队列中的每个指针都标识一个准备好Finalize调用其方法的对象.

在收集(第一个集合)之后,托管堆看起来类似于下图.下面给出的解释::
1.)对象B,G和H占用的内存已被立即回收,因为这些对象没有需要调用的finalize方法.

2.) 但是,对象E,I和J占用的内存无法回收,因为Finalize尚未调用它们的方法. 调用Finalize方法由freacheable队列完成.

3.) A,C,D,F仍然可以通过上面黄色框中的箭头描绘的应用代码来实现,因此在任何情况下都不会收集它们

在此输入图像描述

有一个特殊的运行时线程专用于调用Finalize方法.当可释放队列为空(通常是这种情况)时,该线程会休眠.但是当条目出现时,此线程会唤醒,从队列中删除每个条目,并调用每个对象的Finalize方法.垃圾收集器压缩可回收内存,特殊运行时线程清空可释放队列,执行每个对象的Finalize方法.所以最后是你的Finalize方法执行的时候

下次调用垃圾收集器时(第二次收集),它会看到最终的对象确实是垃圾,因为应用程序的根不指向它,而且可释放的队列不再指向它(它也是EMPTY),因此对象的内存(E,I,J)只需从Heap中回收.参见下图并将其与上图中的数字进行比较

在此输入图像描述

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

注意:: 释放队列被视为根,就像全局和静态变量是根一样.因此,如果对象位于可释放队列上,则该对象是可访问的并且不是垃圾.

作为最后一点,请记住,调试应用程序是一回事,垃圾收集是另一回事,并且工作方式不同.到目前为止,你只能通过调试应用程序来进行FEEL垃圾收集,如果你想调查内存在这里开始的话.