This is a common issue when debugging a program to diagnose finalization issues.
When a debugger is attached to the process, or the process was compiled without optimizations (usually through the DEBUG profile), the lifetime of variables is extended.
Let me give an example:
public void Test()
{
var x = new SomeObject();
x.DoSomething();
// some other lengthy process not involving x
}
In this example, as long as the x
variable is not even mentioned in the rest of the method, after the call to x.DoSomething()
, the variable x
is no longer considered as active and whatever it references can be eligible for collection.
This means that as soon as that method call has returned (I'm simplifying here, the exact lifetime is even more scary than this), the reference to SomeObject
is no longer considered, and the object can be collected by the garbage collector.
This means that you could expect this code to finalize the object before it returns:
public void Test()
{
var x = new SomeObject();
x.DoSomething();
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
}
And indeed, if you compile the project with optimizations on (usually through the RELEASE profile), and then run it outside of the debugger, you can observe that the object is finalized. You would obviously need some way of observing it, such as the output statements in your own code in the question.
But if you run the same program with the debugger attached, or compile it without optimizations, you can think of the code as having an artificial GC.KeepAlive
at the end:
public void Test()
{
var x = new SomeObject();
x.DoSomething();
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
GC.KeepAlive(x);
}
(again, this is a simplification, it is the JITter that handles the variable lifetime analysis and metadata, that simply makes sure metadata reflects the variable being considered alive for the duration of its scope)
and thus x
is indeed not collected. This is what you're observing with your example.
Note that the garbage collector can be thought of as really aggressive, here is an example.
Consider a program that has an IntPtr
handle to something unmanaged, that it needs to clean up. You would do this using Dispose
, and then also add a finalizer, let's take a look:
public class Test : IDisposable
{
private IntPtr _Handle;
public Test()
{
_Handle = <acquire handle to unmanaged resource>
}
~Test()
{
<release unmanaged resource using _Handle>
}
public void Dispose()
{
<release unmanaged resource using _Handle>
_Handle = IntPtr.Zero;
GC.SuppressFinalize(this);
}
}
This looks fine, but let's consider having a simple method that uses this handle:
public string GetName()
{
IntPtr handle = _Handle;
string result;
<read from memory of unmanaged resource into result>
return result;
}
And now we have this code:
var t = new Test();
string x = t.GetName();
and that is the last usage of the t
variable.
The aggressiveness comes from the fact that even during the call to GetName
, there is little use of the object instance. The instance is only used at the very start, reading _Handle
, and then no more, so even during that call GC can kick in and finalize and collect the object while it is trying to read from unmanaged memory.
The correct way to write GetName is to make sure this does not happen:
public string GetName()
{
IntPtr handle = _Handle;
string result;
<read from memory of unmanaged resource into result>
GC.KeepAlive(this);
return result;
}
The garbage collector is scary!