我刚注意到有关垃圾收集的一些非常奇怪的事情。
WeakRef方法按预期收集对象,而async方法报告该对象仍处于活动状态,即使我们已强制进行垃圾回收。有什么想法吗?
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
{
}
答案 0 :(得分:8)
WeakRef方法按预期收集对象
没有理由期待这一点。例如,在Linqpad中尝试,它不会发生在调试版本中,尽管调试和发布版本的其他有效编译都可能有这两种行为。
在编译器和抖动之间,他们可以自由地优化空赋值(毕竟没有使用foo
之后),在这种情况下,GC仍然可以看到线程有一个引用对象而不是收集它。相反,如果没有foo = null
的赋值,他们可以自由地意识到foo
不再被使用,并重新使用持有它的内存或寄存器{{1} (或者确实是完全不同的东西)并收集fooRef
。
因此,既然有foo
,有效的GC都可以将foo = null
视为有根或无根,我们可以合理地预期这两种行为。
尽管如此,所看到的行为是合理的期望可能会发生什么,但不保证值得指出。
好的,除此之外,让我们来看看这里发生了什么。
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();
未被取消,因此GC会将temp0
视为root。
您的代码有两个有趣的变体:
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
立即移动到同一底层方法调用中的下一个事件,即await
。
第二个行为看到收集了GC.Collect()
,因为Foo
在那时返回,然后状态机的await
方法在大约一毫秒之后再次调用。由于这是对幕后方法的新调用,因此没有MoveNext()
的本地引用,因此GC确实可以收集它。
顺便说一下,有一天编译器也不会为那些没有生成Foo
边界的本地生成字段,这将是一个仍会产生正确行为的优化。如果发生这种情况,那么你的两种方法在基本行为方面会变得更加相似,因此在观察到的行为中更有可能相似。