了解.NET中的垃圾收集

dojqjjoe  于 2023-02-17  发布在  .NET
关注(0)|答案(2)|浏览(129)

请看下面的代码:

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

ocebsuys

ocebsuys1#

你在这里被绊倒了,得出了非常错误的结论,因为你正在使用调试器。你需要按照代码在用户机器上运行的方式来运行代码。首先用生成+配置管理器切换到发布版本,将左上角的“活动解决方案配置”组合框更改为“发布”。接下来,进入工具+选项,调试,常规并取消勾选“抑制JIT优化”选项。
现在再次运行你的程序并修改源代码。注意额外的大括号是如何完全没有效果的。注意将变量设置为null是如何完全没有区别的。它总是打印“1”。它现在按照你希望和期望的方式工作。
这就需要解释为什么当你运行调试版本时它的工作方式会如此不同,这就需要解释垃圾收集器是如何发现局部变量的,以及调试器是如何影响它的。
首先,抖动在将方法的IL编译成机器码时执行两个重要的任务,第一个任务在调试器中非常明显,你可以在Debug + Windows + Disassembly窗口中看到机器码。2然而第二个任务是完全不可见的。3它也会生成一个表来描述方法体中的局部变量是如何被使用的。该表为每个方法参数和具有两个地址的局部变量提供了一个条目。变量将首先存储对象引用的地址。变量不再使用的机器码指令的地址。还有变量是存储在堆栈帧还是CPU寄存器上。
这个表对于垃圾收集器来说是必不可少的,它需要知道在执行收集时在哪里查找对象引用。当引用是GC堆上对象的一部分时,这很容易做到。当对象引用存储在CPU寄存器中时,这肯定不容易做到。这个表说明了在哪里查找。
表中的“不再使用”地址非常重要。它使垃圾收集器非常“高效”。它可以收集一个对象引用,即使它是在一个方法中使用的,而那个方法还没有完成执行。这是非常常见的。您的主要方法只会在你的程序终止之前停止执行。显然你不希望在那个Main中使用任何对象引用()方法在程序的持续时间内存活,这将相当于泄漏。抖动可以使用该表来发现这样的局部变量不再有用,这取决于程序在调用Main()方法之前在该方法中的进展程度。
与该表相关的一个近乎神奇的方法是GC.KeepAlive()。这是一个非常特殊的方法,它根本不生成任何代码。它唯一的任务是修改那个表。它延长了局部变量的生存期,防止它存储的引用被垃圾收集。你唯一需要使用它的时候是为了防止GC过于急于收集引用,这可能发生在互操作场景中,其中引用被传递给非托管代码。垃圾收集器无法看到此类代码正在使用此类引用,因为它不是由抖动编译的,因此没有指示在哪里查找引用的表。将委托对象传递给EnumWindows()等非托管函数是何时需要使用GC.KeepAlive()的样板示例。
因此,正如你在Release build中运行了你的示例代码片段后所看到的,局部变量 * 可以 * 在方法完成执行之前被提前收集。更强大的是,如果一个对象的方法不再引用 this,那么它可以在该方法运行时被收集。调试这样一个方法是非常困难的。2因为你可以把变量放在监 windows 口中或者检查它。3而且如果发生GC,它会在你调试的时候消失。4这是非常令人不快的。所以jitter知道有调试器连接,然后修改表,改变“最后使用的”地址,把它从正常值改变为方法中最后一条指令的地址,只要方法没有返回,就保持变量的活动状态,这样你就可以一直监视它,直到方法返回。
这也解释了您之前看到的内容以及您为什么要问这个问题。它打印“0”是因为GC.Collect调用无法收集引用。该表指出,在GC.Collect()调用 * 之后 *,一直到方法的末尾,变量都在使用中。通过运行Debug构建来附加调试器 * 和 *,强制说明这一点。

将变量设置为null现在确实有效果,因为GC将检查变量,并且将不再看到引用。但是请确保您没有落入许多C#程序员已经落入的陷阱,实际上编写代码是毫无意义的。当您在发布版本中运行代码时,无论是否存在该语句都没有任何区别。事实上,抖动优化器将删除该语句,因为它没有任何效果。2所以请确保不要写这样的代码,即使它看起来有效果。
关于此主题的最后一点说明,这就是让编写小程序来处理Office应用程序的程序员遇到麻烦的原因。调试器通常会让他们走错路,他们希望Office程序按需退出。实现此目的的适当方法是调用GC.Collect()。但当他们调试应用时会发现它不起作用,调用Marshal.ReleaseComObject会让他们陷入幻想()。手动内存管理,它很少能正常工作,因为他们很容易忽略一个不可见的接口引用。GC.Collect()实际上能工作,只是在调试应用时不能。

4uqofj5v

4uqofj5v2#

[只是想进一步补充定稿流程的内部内容]
你创建了一个对象,当这个对象被垃圾回收时,这个对象的Finalize方法应该被调用,但是除了这个非常简单的假设,还有更多的事情要做。

概念:

1.未实现Finalize方法的对象:则立即回收它们的存储器,当然,除非应用程序代码不再能够访问它们。
1.实现Finalize方法的对象:需要理解X1 M3 N1 X、X1 M4 N1 X、X1 M5 N1 X的概念,因为它们涉及回收过程。
1.如果应用程序代码无法访问任何对象,则将其视为垃圾。
假设:类/对象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 Framework检测到这些对象具有Finalize方法,并将指向这些对象的指针添加到完成队列

当GC发生时(第一次收集),对象B、E、G、H、I和J被确定为垃圾。A、C、D和F仍然可以通过上面黄色框中箭头所示的应用程序代码访问。
垃圾回收器扫描终结队列,查找指向这些对象的指针。找到指针后,将从终结队列中移除该指针,并将其附加到freachable队列(“F-可达”,可访问队列是由垃圾收集器控制的另一个内部数据结构。freachable队列中的每个指针标识一个准备好调用其Finalize方法的对象。
在第一次GC之后,托管堆看起来类似于下图。解释如下:
1.对象B、G和H占用的内存已被立即回收,因为这些对象没有需要调用的finalize方法。
1.但是,无法回收对象E、I和J占用的内存,因为尚未调用它们的Finalize方法。调用Finalize方法是通过可回收队列完成的。

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

有一个特殊的运行时线程专门用于调用Finalize方法。(通常是这种情况),此线程休眠。但当条目出现时,此线程唤醒,从队列中删除每个条目,并调用每个对象的Finalize方法。垃圾收集器压缩可回收的内存,特殊运行时线程清空可回收的队列,执行每个对象的Finalize方法。这是Finalize方法执行的最后时刻。
下一次调用垃圾收集器(第二次GC)时,它会看到最终化的对象是真正的垃圾,因为应用程序的根不再指向它,可访问队列也不再指向它(它也是空的),因此对象E、I、J的内存可能会从堆中回收。请参见下图,并将其与上图进行比较。

这里需要理解的重要一点是,需要两个GC来回收需要终结的对象所使用的内存。实际上,甚至可能需要两个以上的收集,因为这些对象可能会被提升到更老的代。
注意:可访问队列被认为是根,就像全局变量和静态变量是根一样。因此,如果一个对象在可访问队列上,那么该对象是可访问的,而不是垃圾。
最后一点,记住调试应用程序是一回事,垃圾收集是另一回事,而且工作方式不同。到目前为止,你还不能仅仅通过调试应用程序来感受垃圾收集。如果你想进一步研究内存,请从here开始。

相关问题