了解.NET中的垃圾收集

时间:2013-06-16 05:09:50

标签: c# .net 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();
    }
}

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

3 个答案:

答案 0 :(得分:328)

由于您使用的是调试器,因此您在这里被绊倒并得出了非常错误的结论。您需要按照在用户计算机上运行的方式运行代码。首先使用Build + Configuration manager切换到Release版本,更改" Active解决方案配置"左上角的组合到"发布"。接下来,进入工具+选项,调试,常规并取消勾选"抑制JIT优化"选项。

现在再次运行程序并修改源代码。注意额外的牙箍根本没有效果。并注意如何将变量设置为null根本没有任何区别。它将始终打印" 1"。它现在以您希望的方式工作,并期望它能够发挥作用。

当您运行Debug构建时,其中的任务是解释为什么它的工作原理会有所不同。这需要解释垃圾收集器如何发现局部变量以及如何通过调试器来影响它。

首先,当抖动将方法的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()调用,一直到方法结束。通过运行Debug构建来调试器连接,强迫这样说。

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


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

答案 1 :(得分:31)

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

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

SHORT CONCEPTS ::

  1. 对象没有实现Finalize方法,那就是Memory 立即收回,除非当然,他们无法通过履约表达 应用程序代码

  2. 实施Finalize方法,概念/实施的对象 Application RootsFinalization QueueFreacheable Queue来了 在它们被收回之前。

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

  4. 假设::类/对象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-reachable")。

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

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

    2。)但是,对象E,I和J占用的内存不可能      因为他们的Finalize方法尚未被调用而被收回。      调用Finalize方法由 可释放队列完成。

    3。) A,C,D,F仍可由描述的应用程序代码访问       上面的黄色框中的箭头,所以它们不会被收集       情况下

    enter image description here

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

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

    enter image description here

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

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

    作为最后一点,请记住调试应用程序是一回事,垃圾收集是另一回事,并且工作方式不同。到目前为止,您只能通过调试应用程序来进行垃圾收集,如果您想调查内存获取started here.

答案 2 :(得分:2)

有三种方法可以实现内存管理: -

GC仅适用于托管资源,因此.NET提供Dispose和Finalize来释放非托管资源,如流,数据库连接,COM对象等。

1)处理

必须为实现IDisposable的类型显式调用Dispose。

程序员必须使用Dispose()或使用construct

来调用它

如果您已使用dispose()

,请使用GC.SuppressFinalize(this)来阻止调用Finalizer

2)Finalize或Distructor

在对象符合清理条件后隐式调用它,对象的终结器由终结器线程按顺序调用。

实现终结器的缺点是内存回收被延迟,因为必须在清理之前调用此类/类型的终结器,因此需要额外的回收来回收内存。

3)GC.Collect()

使用GC.Collect()并不一定要将GC用于收集,GC仍然可以覆盖并随时运行。

GC.Collect()也只会运行垃圾收集的跟踪部分,并将项目添加到终结器队列,但不会为其他线程处理的类型调用终结器。

如果要确保在调用GC.Collect()之后调用所有终结器,请使用WaitForPendingFinalizers