Myth: Keeping a reference to an object in a variable prevents the
finalizer from running while the variable is alive; a local variable
is always alive at least until control leaves the block in which the
local was declared.
{
Foo foo = new Foo();
Blah(foo);
Bar();
}
The C# specification states that the runtime is permitted broad latitude to
detect when storage containing a reference is never going to be accessed
again, and to stop treating that storage as a root of the garbage collector.
For example, suppose we have a local variable foo
and a reference is written
into it at the top of the block. If the jitter knows that a particular read is
the last read of that variable, the variable can legally be removed from the
set of GC roots immediately; it doesn’t have to wait until control leaves the
scope of the variable. If that variable contained the last reference then the
GC can detect that the object is unreachable and put it on the finalizer queue
immediately. Use GC.KeepAlive
to avoid this.
Why does the jitter have this latitude? Suppose the local variable is
enregistered into the register needed to pass the value to Blah()
. If foo
is in a register that Bar()
needs to use, there’s no point in saving the
value of the never-to-be-read-again foo
on the stack before Bar()
is
called. (If the actual details of the code generated by the jitter is of
interest to you, see Raymond Chen’s deeper analysis of this issue.)
Extra bonus fun: the runtime uses less aggressive code generation and
less aggressive garbage collection when running the program in the
debugger, because it is a bad debugging experience to have objects
that you are debugging suddenly disappear even though the variable
referring to the object is in scope. That means that if you have a bug
where an object is being finalized too early, you probably cannot
reproduce that bug in the debugger!
See the last point in this article for an even more horrid version of
this problem.
Y
是对X
唯一的引用,但Y
变成孤儿了,那么Y
和X
都将被销毁。由于顺序不能保证,所以X
可能会在Y
之前被销毁。 - Pieter Witvoet