我刚刚注意到有关垃圾收集的一些非常奇怪的事情.
WeakRef方法按预期收集对象,而异步方法报告对象仍然存活,即使我们已强制进行垃圾回收.有什么想法吗?
class Program
{
static void Main(string[] args)
{
WeakRef();
WeakRefAsync().Wait();
}
private static void WeakRef()
{
var foo = new Foo();
WeakReference fooRef = new WeakReference(foo);
foo = null;
GC.Collect();
Debug.Assert(!fooRef.IsAlive);
}
private static async Task WeakRefAsync()
{
var foo = new Foo();
WeakReference fooRef = new WeakReference(foo);
foo = null;
GC.Collect();
Debug.Assert(!fooRef.IsAlive);
}
}
public class Foo
{
}
Run Code Online (Sandbox Code Playgroud)
WeakRef方法按预期收集对象
没有理由期待这一点.尝试在Linqpad中,它不会在调试版本中发生,例如,虽然调试和发布版本的其他有效编译可能有任何行为.
在编译器和抖动之间,它们可以自由地优化掉空分配(foo毕竟没有任何用途),在这种情况下,GC仍然可以看到线程具有对象的引用而不是收集它.相反,如果没有任何分配foo = null他们可以自由地意识到foo不再使用并重新使用已经持有它fooRef(或者确实完全是为了其他东西)并收集的记忆或注册foo.
因此,既然有或没有foo = null它对GC有效,可以foo看作是root还是root,我们可以合理地预期这两种行为.
尽管如此,所看到的行为对于可能发生的事情是一个合理的期望,但是不能保证值得指出.
好吧,除此之外,让我们来看看这里发生了什么.
该async方法生成的状态机是一个结构,其中的字段对应于源中的本地.
所以代码:
var foo = new Foo();
WeakReference fooRef = new WeakReference(foo);
foo = null;
GC.Collect();
Run Code Online (Sandbox Code Playgroud)
有点像:
this.foo = new Foo();
this.fooRef = new WeakReference(foo);
this.foo = null;
GC.Collect();
Run Code Online (Sandbox Code Playgroud)
但是现场访问总是在本地进行.所以在这方面它几乎就像:
var temp0 = new Foo();
this.foo = temp0;
var temp1 = new WeakReference(foo);
this.fooRef = temp1;
var temp2 = null;
this.foo = temp2;
GC.Collect();
Run Code Online (Sandbox Code Playgroud)
而且temp0还没有被淘汰,所以GC找到了Foo根源.
您的代码的两个有趣变体是:
var foo = new Foo();
WeakReference fooRef = new WeakReference(foo);
foo = null;
await Task.Delay(0);
GC.Collect();
Run Code Online (Sandbox Code Playgroud)
和:
var foo = new Foo();
WeakReference fooRef = new WeakReference(foo);
foo = null;
await Task.Delay(1);
GC.Collect();
Run Code Online (Sandbox Code Playgroud)
当我运行它时(同样,处理本地的内存/寄存器的方式的合理差异可能导致不同的结果)第一个具有相同的行为,因为当它调用另一个Task方法await时,该方法返回一个完成的任务,所以await立即移动到同一个底层方法调用中的下一个东西,即GC.Collect().
第二个具有查看Foo收集的行为,因为此时的await返回然后状态机的MoveNext()方法大约在一毫秒之后再次被调用.由于这是对幕后方法的新调用,因此没有本地引用,Foo因此GC确实可以收集它.
顺便提一下,有一天编译器也不会为那些没有跨越await边界的本地生成字段,这将是一种仍然会产生正确行为的优化.如果发生这种情况,那么您的两种方法在基本行为方面会变得更加相似,因此在观察到的行为中更可能类似.