考虑下面的代码:
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()调用时没有被其他任何对象进一步引用,但为什么还没有在其中最终确定呢?
GC.Collect()
由于正在使用调试器,您在这里绊倒了,得出了非常错误的结论。您需要以在用户计算机上运行的方式运行代码。首先使用Build + Configuration Manager切换到Release build,将左上角的“ 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版本中运行示例片段后 所 看到的那样,可以在方法完成执行之前及早收集局部变量。更强大的是,如果对象的方法之一不再引用 this ,则该对象可以在其方法之一运行时被收集。这样做有一个问题,调试这种方法很尴尬。因为您可以将变量放入“监视”窗口中或进行检查。如果发生GC,在调试时它将 消失 。那将是非常不愉快的,因此抖动会 意识到 已附加调试器。然后 修改 表格并更改“上次使用”地址。并将其从其正常值更改为该方法中最后一条指令的地址。只要方法没有返回,它就使变量保持活动状态。这样您就可以继续观察它,直到方法返回为止。
现在,这也解释了您之前看到的内容以及为什么提出该问题。因为GC.Collect调用无法收集引用,所以它显示“ 0”。该表说,该变量在使用 过去 的GC.Collect的()调用,直到方法结束所有的方式。通过附加调试器 并 运行Debug构建来强制这样说。
现在将变量设置为null确实有效,因为GC将检查该变量并且不再看到引用。但是请确保您不会陷入许多C#程序员陷入的陷阱,实际上编写该代码是没有意义的。无论在运行Release版本中的代码时是否存在该语句都没有关系。实际上,抖动优化器将 删除 该语句,因为它根本不起作用。因此,即使 看起来 有效果,也请不要编写这样的代码。
关于此主题的最后一点说明,这就是程序员遇到麻烦,他们编写小型程序来使用Office应用程序执行操作。调试器通常将它们放在错误的路径上,他们希望Office程序按需退出。适当的方法是调用GC.Collect()。但是他们会在调试应用程序时发现它不起作用,并通过调用Marshal.ReleaseComObject()将其引导到永无止境的土地。手动内存管理,它很少能正常工作,因为它们很容易忽略不可见的接口引用。GC.Collect()实际上有效,只是在调试应用程序时不起作用。