WeakRef方法按预期收集对象。
没有理由期望这样。例如,在Linqpad中尝试,在调试版本中不会发生,尽管其他有效的编译(包括调试和发布版本)可能具有任一行为。
在编译器和Jitter之间,它们可以自由地优化空值赋值(毕竟没有任何东西使用foo),在这种情况下,GC仍然可以看到线程具有对对象的引用并且不收集它。相反,如果没有
foo = null
的赋值,它们可以自由地意识到
foo
不再使用,并重新使用曾经持有它的内存或寄存器来保存
fooRef
(或者实际上是用于其他任何事情),并收集
foo
。
因此,既可以通过
foo = null
,也可以不通过
foo = null
,使GC将
foo
视为已根或未根,我们可以合理地期望任一行为。
尽管如此,所看到的行为是一个关于
可能会发生的
合理期望,但值得指出的是,这并不是保证。
好的,除此之外,让我们看看这里实际发生了什么。
由
async
方法产生的状态机是一个具有与源中局部变量对应的字段的结构体。
因此,代码:
var foo = new Foo();
WeakReference fooRef = new WeakReference(foo);
foo = null;
GC.Collect();
有点像:
this.foo = new Foo();
this.fooRef = new WeakReference(foo);
this.foo = null;
GC.Collect();
但是字段访问总是在本地进行某些操作。因此,在这方面,它几乎像:
var temp0 = new Foo();
this.foo = temp0;
var temp1 = new WeakReference(foo);
this.fooRef = temp1;
var temp2 = null;
this.foo = temp2;
GC.Collect();
而且temp0
没有被清零,所以垃圾回收器发现Foo
被视为根。
你的代码有两个有趣的变体:
var foo = new Foo();
WeakReference fooRef = new WeakReference(foo);
foo = null;
await Task.Delay(0);
GC.Collect();
还有:
var foo = new Foo();
WeakReference fooRef = new WeakReference(foo);
foo = null;
await Task.Delay(1);
GC.Collect();
当我运行它时(同样,处理本地变量内存/寄存器的合理差异可能导致不同的结果),第一个示例的行为与您的示例相同,因为它调用另一个
Task
方法并等待它,该方法返回已完成的任务,因此
await
立即移动到同一基础方法调用中的下一个内容,即
GC.Collect()
。
第二个示例的行为是看到
Foo
被回收,因为
await
在那一点上返回,然后状态机大约一毫秒后再次调用其
MoveNext()
方法。由于这是对幕后方法的新调用,因此没有对
Foo
的局部引用,因此GC确实可以将其回收。
顺便说一句,编译器有可能有一天不会为那些跨越
await
边界的本地变量产生字段,这将是一种优化,仍会产生正确的行为。如果发生这种情况,那么您的两种方法在底层行为上将变得更加相似,因此更有可能在观察到的行为上更相似。
async
方法中的foo
和fooRef
变成了编译器生成的状态机类的属性。但我尝试将这些变量包装在另一个类中,它们确实被收集了...有没有办法询问GC哪里/谁仍然引用Foo
实例? - René Vogtfoo
是该类的成员。因此,只要表示您的方法的对象仍然存在,它的成员就不会被垃圾回收。 - Matteo UmiliDebug.Assert
更改为Console.WriteLine
并在主函数末尾添加一个ReadLine
,则在调试构建中会打印出“True False”,而在发布构建中会打印出“True True”。也许与调试时局部变量的GC生命周期延长方式有关。 - Damien_The_Unbelieverfoo = null;
是一个错误。了解 为什么它是一个错误 可以让您了解 C# 编译器如何重写异步方法,从而获得不同的结果。 - Hans Passantawait
才是真正的 bug。在实际代码中,将someLocal = null
设置为空几乎没有什么价值,但是在async
方法(或者使用yield
的方法)中,foo
是一个在调用之间存在的字段,因此它确实可能有一些值。如果在其中添加了一些await
,那么这不再是一个 bug,而是一个过早的优化。 - Jon Hanna