请考虑以下代码:

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

推荐答案

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

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

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

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

该表对于垃圾回收器至关重要,它需要知道在执行回收时在哪里查找对象引用.当引用是GC堆上的对象的一部分时,很容易做到这一点.当对象引用存储在CPU寄存器中时,绝对不容易做到这一点.桌子上写着go 哪里找.

表中的"不再使用"地址非常重要.这使得垃圾收集器非常容易收集垃圾.它可以收集对象引用,即使它在一个方法中使用,而该方法尚未完成执行.这很常见,例如Main()方法只会在程序终止之前停止执行.很明显,您不希望Main()方法中使用的任何对象引用在程序运行期间有效,这相当于泄漏.jitter可以使用该表来发现这样的局部变量不再有用,这取决于程序在调用Main()方法之前在该方法中的进度.

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

因此,正如您在发布版本中运行示例代码段后所看到的,在方法完成执行之前,局部变量can会被提前收集.更强大的是,如果一个对象的某个方法不再引用this,则该对象可以在其方法运行时被收集.这其中有一个问题,调试这样的方法非常尴尬.因为你可以把变量放在观察窗口或判断它.在调试过程中,如果发生GC,则会发生错误.这将是非常不愉快的,所以抖动是aware,因为有一个调试器连接.然后它打开表格并更改"上次使用"的地址.并将其从正常值更改为方法中最后一条指令的地址.只要该方法没有返回,它就可以使变量保持活动状态.它允许您一直监视它,直到方法返回.

现在,这也解释了您之前看到的情况以及为什么要问这个问题.它输出"0",因为EgCollect调用不能收集引用.该表显示该变量正在使用past,一直到方法的末尾,都是在进行EgCollect()调用.通过运行调试版本附加调试器and来强制这样说.

将变量设置为null现在确实有效,因为GC将判断变量,并且不再看到引用.但要确保你没有落入许多C#程序员落入的trap ,实际上编写代码是毫无意义的.在发布版本中运行代码时,该语句是否存在没有任何区别.事实上,抖动优化器将删除该语句,因为它没有任何效果.因此,请确保不要编写这样的代码,即使这可能会产生影响.


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

.net相关问答推荐

DI通过对象的接口而不是实际类型来解析服务

从没有流的EventStore中读取流不存在异常

在 Inno Setup 中判断给定服务的依赖服务

从 Contentful 中的富文本元素中获取价值?

.NET Async / Await:状态机如何知道何时继续执行?

信号量的多线程问题

查找所有源硬编码字符串

调整小数精度,.net

您如何确定两个 HashSet 是否相等(按值,而不是按引用)?

如何获取控制台应用程序的执行目录

IIS Express - 500.19 无法读取配置文件 - 因为它正在查看错误的路径

C# 中基于接口编程的运算符重载

是否可以更改 Winforms 组合框以禁用输入?

.NET 如何判断路径是否是文件而不是目录?

如何在 C# 中仅设置 DateTime 变量的时间部分

如何对 LINQ to XML 中的元素进行深层复制?

SqlBulkCopy 的推荐批量大小是多少?

当它被抛出和捕获时,不要在那个异常处停止调试器

NServiceBus 与 MassTransit

序列化一个可为空的 int