了解.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方法中的variablesc1超出了作用域,并且在GC.Collect()
时没有被任何其他对象进一步引用,为什么在那里没有最终确定呢?
由于您正在使用debugging器,因此您在此处被绊倒并且得出错误的结论。 您需要按照在用户计算机上运行的方式运行代码。 先用Build + Configuration manager切换到Release版本,将左上angular的“Active solution configuration”组合改为“Release”。 接下来,进入工具+选项,debugging,常规,并取消“抑制JIT优化”选项。
现在再次运行你的程序,并修改源代码。 注意额外的大括号怎么样都没有效果。 并注意如何设置variables为null根本没有任何区别。 它将始终打印“1”。 它现在以你期望的方式工作,并且预计它会起作用。
当你运行Debug版本的时候,解释为什么它的工作原理是不同的。 这就要求解释垃圾收集器如何发现局部variables以及如何通过debugging器的存在来影响垃圾收集器。
首先,抖动在将IL编译成机器码时执行两个重要的任务。 第一个在debugging器中非常明显,您可以在Debug + Windows + Disassembly窗口中看到机器代码。 然而,第二项责任完全是不可见的。 它还会生成一个描述方法体内局部variables如何使用的表。 该表有两个地址的每个方法参数和局部variables的条目。 variables将首先存储对象引用的地址。 机器代码指令的地址,该variables不再使用。 此外,无论该variables是存储在堆栈帧还是一个cpu寄存器。
这个表对于垃圾收集器来说是必不可less的,它在执行收集时需要知道在哪里查找对象引用。 当引用是GC堆上的对象的一部分时,很容易做到。 将对象引用存储在CPU寄存器中时绝对不容易。 表格说了要看的地方。
表中的“不再使用”地址非常重要。 它使垃圾收集器非常高效 。 它可以收集一个对象引用,即使它在一个方法中使用,并且该方法还没有完成执行。 这是很常见的,例如你的Main()方法只会在你的程序终止之前停止执行。 显然你不希望在该Main()方法内部使用的任何对象引用在程序的持续时间内生存,这相当于泄漏。 抖动可以使用表来发现这样一个局部variables不再有用,这取决于程序在调用之前在Main()方法内进展了多less。
与该表相关的一个几乎神奇的方法是GC.KeepAlive()。 这是一个非常特殊的方法,它根本不会生成任何代码。 它唯一的责任就是修改那张表。 它延长了局部variables的生命周期,防止它存储的引用被垃圾收集。 唯一需要使用它的方法是停止GC过度收集引用,这可能会在引用传递给非托pipe代码的互操作场景中发生。 垃圾收集器不能看到这样的代码使用的引用,因为它不是由抖动编译的,所以没有表格说明在哪里查找引用。 将委托对象传递给像EnumWindows()这样的非托pipe函数是您需要使用GC.KeepAlive()时的样板示例。
所以,正如你在Release版本中运行它之后你可以从样例代码中看到的那样,在方法执行完成之前,局部variables可以被提前收集。 更强大的是,一个对象可以在其中一个方法运行时收集,如果该方法不再提及这个方法的话。 有一个问题,debugging这种方法是非常尴尬的。 既然你可能把variables放在监视窗口中或检查它。 如果发生GC,它将在debugging过程中消失 。 这将是非常不愉快的,所以抖动意识到有附加的debugging器。 然后修改表格并更改“上次使用”的地址。 并将其从正常值更改为方法中最后一个指令的地址。 只要方法没有返回,它就保持variables存活。 这可以让你继续观看,直到方法返回。
这现在也解释了你之前看到的以及你为什么问这个问题。 它打印“0”,因为GC.Collect调用不能收集引用。 该表格表示,该variables正在使用, 通过 GC.Collect()调用,一直到方法结束。 强制通过附加debugging器并运行Debug版本来这样说。
将variables设置为null现在有效果,因为GC将检查该variables并将不再看到引用。 但是请确保你不会陷入许多C#程序员陷入的陷阱,实际上编写代码是毫无意义的。 无论是否在Release版本中运行代码时,该语句是否存在都没有区别。 事实上,抖动优化器将删除该声明,因为它没有任何效果。 所以一定不要这样写代码,即使它看起来有效果。
关于这个主题的最后一个注意事项,就是让程序员陷入麻烦,编写一些小程序来处理Office应用程序。 debugging器通常会让他们进入错误的path,他们希望Office程序按需退出。 适当的方法是通过调用GC.Collect()。 但是他们会发现在debugging他们的应用程序的时候,它不起作用,通过调用Marshal.ReleaseComObject()将他们引导到永远不会降落的地方。 手动内存pipe理,它很less正常工作,因为他们很容易忽略一个看不见的接口引用。 GC.Collect()实际上工作,而不是当你debugging应用程序。
[只是想进一步添加终止内核的过程]
所以,你创build一个对象,当对象被收集时,应该调用对象的Finalize
方法。 但是,这个非常简单的假设还有更多的定案。
简短的概念::
-
对象没有实现
Finalize
方法,有内存立即收回,除非当然,他们不能通过
应用程序代码了 -
实现
Finalize
方法的对象,Application Roots
的概念/实现,Freacheable Queue
,Freacheable Queue
来之前,他们可以回收。 -
任何对象被认为是垃圾,如果它不能通过应用程序代码来重新获得
假设:类/对象A,B,D,G,H不执行Finalize
方法,C,E,F,I,J实现Finalize
方法。
当一个应用程序创build一个新的对象时,新的操作符从堆中分配内存。 如果对象的types包含Finalize
方法,则指向该对象的指针将被放置在最终化队列中 。
因此指向对象C,E,F,I,J的指针被添加到最终化队列中。
最终化队列是由垃圾收集器控制的内部数据结构。 队列中的每个条目都指向一个对象,该对象在回收对象的内存之前应调用其Finalize
方法。 下图显示了一个包含多个对象的堆。 其中一些对象可从应用程序的根目录中获得 ,有些则不可用。 当创build对象C,E,F,I和J时,.Net框架检测到这些对象具有Finalize
方法,并将指向这些对象的指针添加到最终化队列中 。
当发生GC时(第1次收集),确定对象B,E,G,H,I和J是垃圾。 因为A,C,D,F仍然可以通过上面黄色框中箭头所示的应用程序代码来重新获得。
垃圾回收器扫描结束队列,查找指向这些对象的指针。 当find一个指针时,指针将从最终化队列中移除并附加到可扩展队列 (“F-reachable”)。
可变空间队列是垃圾收集器控制的另一个内部数据结构。 可freachable队列中的每个指针标识一个准备好调用Finalize
方法的对象。
收集之后(第一次收集),托pipe堆看起来类似于下图。 下面给出的解释::
1) 被对象B,G和H占用的内存已被立即回收,因为这些对象没有需要被调用的finalize方法 。
2.) 但是,对象E,I和J占用的内存不能被回收,因为它们的Finalize
方法尚未被调用。 调用Finalize方法由freacheable队列完成。
3.) A,C,D,F仍然可以通过上面的黄色框箭头描述的应用代码来实现,所以在任何情况下都不会被收集
有一个专用于调用Finalize方法的特殊运行时线程。 当freachable队列是空的(通常是这种情况),这个线程睡觉。 但是当条目出现时,这个线程会唤醒,从队列中移除每个条目,并调用每个对象的Finalize方法。 垃圾收集器压缩可回收的内存,特殊的运行时线程清空freachable队列,执行每个对象的Finalize
方法。 所以最终当你的Finalize方法被执行的时候
下一次调用垃圾收集器(第二个集合)时,它会看到最终化的对象是真正的垃圾,因为应用程序的根并不指向它,可扩展队列不再指向它(它也是EMPTY),因此对象(E,I,J)的内存从堆中简单回收。参见下图,并将其与上图
这里需要了解的重要一点是需要两个GC来回收需要完成的对象所使用的内存。 实际上,甚至需要两个以上的collections室,因为这些物品可能会被提升到老一代
注意: freachable队列被认为是一个像全局一样的根,静态variables是根。 因此,如果一个对象位于freachable队列中,则该对象是可访问的,而不是垃圾。
作为最后一个注意事项,请记住,debugging应用程序是一回事,垃圾收集是另一回事,工作方式不同。 到目前为止,您不能通过debugging应用程序来FEEL垃圾回收,进一步如果你想调查内存开始在这里。
有3种方法可以实现内存pipe理:
GC仅适用于托pipe资源,因此.NET提供Dispose和Finalize来释放非托pipe资源,如stream,数据库连接,COM对象等。
1)处置
对于实现IDisposable的types,必须显式调用Dispose。
程序员必须使用Dispose()或Using构造函数来调用它
使用GC.SuppressFinalize(this)来防止调用Finalizer(如果已经使用了dispose()
2)结束或分解
它在对象符合清理之后被隐式地调用,终止器线程依次调用对象的终结器。
实现终结器的缺点是内存回收被延迟,因为这样的类/types的终结器必须事先被清除,所以需要额外的回收来回收内存。
3)GC.Collect()
使用GC.Collect()不一定把GC用于收集,GC仍然可以覆盖并随时运行。
GC.Collect()也只会运行垃圾回收的追踪部分,并将项目添加到终结器队列中,但不会调用由另一个线程处理的types的终结器。
如果您想确保在调用GC.Collect()之后调用所有终结器,请使用WaitForPendingFinalizers。
请参阅我的博客文章,我有这篇文章: – 垃圾收集在.NET中