我有一个班级:
public class SomeClass {
public int I;
public SomeClass(int input) {
I = input;
Console.WriteLine("I = {0}", I);
}
~SomeClass() {
Console.WriteLine("deleted");
}
public void Foo() {
Thread.Sleep(1000);
Console.WriteLine("Foo");
}
}
和这个程序:
class Program {
static void Main(string[] args) {
new Thread(() => {
Thread.Sleep(100);
GC.Collect();
}) { IsBackground = true }.Start();
new SomeClass(10).Foo();
// The same as upper code
// var t = new SomeClass(10);
// t.Foo();
}
}
当我在调试模式中运行此代码时,我有下一个结果:
I = 10
Foo
deleted
但是,当我将模式更改为发布时,结果会更改为:
I = 10
deleted
Foo
据我了解,与call
和callvirt
存在差异:当优化以发布模式启动时,编译器会查看Foo
方法并且可以&#39 ; t在此方法中找到对SomeClass
的任何引用,以及此方法按地址调用静态方法的原因,垃圾收集器可以收集此对象。
否则,如果我更改Foo
方法(例如,将Console.WriteLine(I)
添加到此方法中),编译器不会决定将此方法称为call
,它应该是由指向callvirt
和垃圾收集器的实例调用,不会收集此对象。
你能否更清楚地解释一下,这里发生了什么(为什么 GC 可以收集对象,如果是,那么方法调用如何)。
答案 0 :(得分:5)
我怀疑这与call
和callvirt
有什么关系。
我强烈怀疑这只是因为你没有使用SomeClass.Foo
中的任何字段。当确定不会再次引用任何数据时,垃圾收集器可以自由地收集对象 - 因此没有代码会查看对象的任何引用或对象中的任何字段。
基本上,如果您编写终结器并且需要确保在该对象中的方法运行时对象未完成,则需要非常小心。你可以在方法结束时使用GC.KeepAlive(this)
作为一种方法,但它有点难看。如果可以的话,我会非常努力地避免需要终结器。我记不清上次写一篇了。 (有关详情,请参阅Joe Duffy's blog。)
如果附加了调试器,那么GC对于它可以收集的东西很多不那么激进 - 毕竟,如果你可以在任何时候进入调试器并检查任何对象的字段正在运行的实例方法的目标,它消除了垃圾收集这些对象的可能性。
答案 1 :(得分:1)
当你进行调试时,整个系统 1 会延长对象的生命周期。
所以,当一个线程正在执行这个方法时:
public void Foo() {
Thread.Sleep(1000);
Console.WriteLine("Foo");
}
此方法不使用this
。因此,在不调试时,在此方法开始运行后,它不再需要该对象存在。
但是,在调试时,您可能已经在Console.WrtieLine
方法上设置了断点,并从那里决定检查this
。因此,系统合谋保持this
活着(以及在方法体内不再使用的任何局部变量)。
1 这个旧presentation显示了JIT和GC实际上如何协同工作以确定哪些引用是实时的(参见幻灯片30以后)。我的理解是JIT做了更多的工作来帮助调试 - 不重用它可能的局部变量槽(因此值仍然是可见的)并通知GC所有变量在整个方法中都是活着的,而不是更多它可以提供零碎的分析。