Warning: file_get_contents(/data/phpspider/zhask/data//catemap/2/.net/21.json): failed to open stream: No such file or directory in /data/phpspider/zhask/libs/function.php on line 167

Warning: Invalid argument supplied for foreach() in /data/phpspider/zhask/libs/tag.function.php on line 1116

Notice: Undefined index: in /data/phpspider/zhask/libs/function.php on line 180

Warning: array_chunk() expects parameter 1 to be array, null given in /data/phpspider/zhask/libs/function.php on line 181
C# 理解.NET中的垃圾收集_C#_.net_Garbage Collection - Fatal编程技术网

C# 理解.NET中的垃圾收集

C# 理解.NET中的垃圾收集,c#,.net,garbage-collection,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 commen

考虑以下代码:

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

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

您在这里被绊倒了,并且得出了非常错误的结论,因为您使用的是调试器。您需要以代码在用户机器上运行的方式运行代码。使用build+Configuration manager首先切换到发布版本,将左上角的活动解决方案配置组合更改为发布。接下来,进入“工具+选项”、“调试”、“常规”并取消选中“抑制JIT优化”选项

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

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

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

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

表中不再使用的地址非常重要。它使垃圾收集器非常高效。它可以收集一个对象引用,即使它在一个方法中使用,而该方法还没有完成执行。这是很常见的,例如,您的Main方法只会在程序终止之前停止执行。很明显,您不希望在主方法中使用的任何对象引用在程序期间有效,这将导致泄漏。jitter可以使用该表来发现这样一个局部变量不再有用,这取决于程序在发出调用之前在该主方法中的进度

与该表相关的一个几乎神奇的方法是GC.KeepAlive。这是一个非常特殊的方法,它根本不生成任何代码。它唯一的职责就是修改那张表。它延长了局部变量的生存期,防止它存储的引用被垃圾回收。您需要使用它的唯一时间是阻止GC过于急切地收集引用,这可能发生在互操作场景中,其中引用被传递给非托管代码。垃圾收集器无法看到这样的代码正在使用这样的引用,因为它不是由jitter编译的,所以没有说明在何处查找引用的表。向非托管函数(如EnumWindows)传递委托对象是需要使用GC.KeepAlive的样板示例

因此,正如您在发布版本中运行示例代码段后可以从中看出的那样,可以在方法完成执行之前提前收集局部变量。更强大的是,如果某个对象的某个方法不再引用该对象,则可以在该对象的某个方法运行时收集该对象。这有一个问题,调试这样的方法是非常尴尬的。因为你可以把变量放在观察窗口或者检查它。如果发生GC,它将在调试时消失。这将是非常不愉快的,因此jitter知道存在附加的调试器。然后它修改表并更改最后使用的地址。并将其从正常值更改为方法中最后一条指令的地址。只要方法没有返回,它就可以保持变量的活动状态。它允许您一直监视它,直到方法返回为止

这也解释了你之前看到的以及你为什么问这个问题。它打印0,因为GC.Collect调用无法收集引用。该表显示,该变量一直在使用中,直到方法结束为止,都是通过GC.Collect调用使用的。通过连接调试器并运行调试生成,强制这样说

将变量设置为null确实会产生错误 现在生效,因为GC将检查变量,不再看到引用。但是要确保你没有落入许多C程序员所落入的陷阱,实际上编写代码是毫无意义的。在发布版本中运行代码时,该语句是否存在没有任何区别。事实上,抖动优化器将删除该语句,因为它没有任何效果。因此,请确保不要编写这样的代码,即使它似乎有效果


关于这个主题的最后一个注意事项是,这会让编写小程序来使用Office应用程序的程序员陷入麻烦。调试器通常使他们走错了路,他们希望Office程序按需退出。适当的方法是调用GC.Collect。但当他们调试自己的应用程序时,他们会发现它不起作用,通过调用Marshal.ReleaseComObject将他们引入“永不着陆”。手动内存管理,它很少正常工作,因为它们很容易忽略不可见的接口引用。GC.Collect实际上是有效的,只是在调试应用程序时不起作用。

[只是想进一步添加最终确定过程的内部内容]

因此,您创建了一个对象,当收集该对象时,应该调用该对象的Finalize方法。但是,除了这个非常简单的假设之外,还有更多的事情要完成

简短概念:

对象没有实现Finalize方法,内存不足 立即回收,当然,除非 应用程序代码了吗

实现Finalize方法的对象,概念/实现 应用程序根、终结队列、Freacheable队列 在它们被回收之前

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

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

当应用程序创建新对象时,新操作符从堆中分配内存。如果对象的类型包含Finalize方法,则指向该对象的指针将放置在Finalize队列上。 因此,指向对象C、E、F、I、J的指针被添加到终结队列中。 终结队列是由垃圾收集器控制的内部数据结构。队列中的每个条目都指向一个对象,在回收该对象的内存之前,应该调用该对象的Finalize方法。 下图显示了包含多个对象的堆。其中有些对象可以从应用程序的根访问,有些则不能。创建对象C、E、F、I和J时,.Net framework会检测到这些对象具有Finalize方法,并将指向这些对象的指针添加到Finalize队列中

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

垃圾收集器扫描终结队列,寻找指向这些对象的指针。找到指针后,该指针将从终结队列中移除,并附加到freachable队列F-reachable。freachable队列是由垃圾收集器控制的另一个内部数据结构。freachable队列中的每个指针标识一个对象,该对象已准备好调用其Finalize方法

在collection1st集合之后,托管堆看起来与下图类似。解释如下: 1.对象B、G和H占用的内存已被回收 因为这些对象没有一个finalize方法 需要打电话

二,。但是,对象E、I和J占用的内存无法恢复 已回收,因为尚未调用其Finalize方法。 通过freacheable队列调用Finalize方法

三,。A、 C、D、F仍然可以通过中描述的应用程序代码访问 上面黄色框中的箭头,因此它们不会被收集到任何位置 案例

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

下次调用垃圾收集器第二次收集时,它会看到最终确定的对象是真正的垃圾,因为应用程序的根不指向它,并且freachable队列也不再指向它的空,因此对象的内存E,I,J只是从堆中回收的。请参见下图,并将其与上图进行比较

重要的是要了解他 re是指需要两个GC来回收需要终结的对象所使用的内存。实际上,甚至可能需要两个以上的集合,因为这些对象可能会升级到较旧的一代

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


最后,请记住调试应用程序是一回事,垃圾收集是另一回事,其工作方式不同。到目前为止,仅仅通过调试应用程序还不能感觉到垃圾回收,如果您希望调查内存获取,GC不会在实例超出范围时立即释放实例。当它认为有必要的时候就会这样做。您可以在此处阅读有关GC的所有内容:@user1908061 Pssst。你的链接已断开。一些文章:| | | | | | | | | |另请参阅我的问题,汉斯很好地回答了我@HansPassant我刚刚找到了这个很棒的解释,它也回答了我的部分问题:关于GC和线程同步。我还有一个问题:我想知道GC是在挂起时压缩并更新存储在内存中的寄存器中使用的地址,还是跳过这些地址?在恢复之前挂起线程后更新寄存器的进程对我来说就像是一个被操作系统阻塞的严重安全线程。线程挂起时,GC更新CPU寄存器的备份存储。一旦线程恢复运行,它现在将使用更新的寄存器值。@HansPassant,如果您添加对您在此处描述的CLR垃圾收集器的一些不明显细节的引用,我将不胜感激。从配置角度看,一个重要的问题是启用了.csproj中的Optimize code true。这是版本配置中的默认设置。但在使用自定义配置的情况下,需要知道此设置很重要。