'Finalizer not called after GC.WaitForPendingFinalizers
I read this fantastic explanation from Eric Lippert concerning when an object, having a reference to another one via an event, is garbage-collected.
To prove what Eric said, I tried out this code:
using System;
public class Program
{
public static void Main()
{
{
var myClass = new GCClass();
LongLivingClass.MyEvent += myClass.HandleEvent;
} // LongLivingClass holds a reference to myClass
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
// expect: finalizer of GCLass not run
{
var myClass = new GCClass();
myClass.MyEvent += LongLivingClass.HandleEvent;
} // myClass can easily be GCed here
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
// expect: finalizer should run
Console.WriteLine("Finish");
}
}
class GCClass
{
public event EventHandler MyEvent;
public void HandleEvent(object o, EventArgs e) { }
~GCClass() { Console.WriteLine("Finalizer hit"); }
}
public class LongLivingClass
{
public static event EventHandler<EventArgs> MyEvent;
public static void HandleEvent(object o, EventArgs e) { }
}
As I expected the first GC.Collect
-block doesn't finalize anything, as the object simply is referenced by LongLvongClass
and thus survives a collection.
The second block however also does not call the finalizer, although myClass
is eligible for collection and we're even waiting for the finalizer to happen. However, my finalizer wasn't hit. As from GC.Collect() and Finalize I'd expect the finalizer to be hit here. I put a breakpoint into it, to show that.
Where did I go wrong here? I suppose myClass
is not collected in the second code-block, but I don't know why.
Solution 1:[1]
Where did I go wrong here?
Short version:
The important thing to realize here is that C# is not like C++ where }
means "we must run destructors of locals now".
C# is allowed to increase the lifetime of any local variable at its discretion, and it does so all the time. Therefore you should never expect a local variable's contents to be collected just because control enters a region where the variable is out of scope.
Long version:
The rule that { }
defines a nested local variable declaration space is a rule of the C# language; it is emphatically NOT a feature of the IL that C# is compiled to!
Those local variable declaration spaces are there so that you can organize your code better and so that the C# compiler can find bugs where you used a local variable when it was not in scope. But C# does NOT emit IL that says "the following local variables are now out of scope" at the bottom of your { }
block.
The jitter is allowed to notice that your second myClass
is never read from and therefore could be removed from the root set after the final write. But the fact that it is allowed to do so does not require it to do so, and typically it will not.
That "typically" is doing some heavy lifting there because, of course, the jitter is allowed to shorten the lifetime of a local. Consider this bad situation:
{
ManagedHandle handle = NativeObject.Open();
SomeNativeCode.CacheTheUnderlyingHandle(handle);
// handle is still in scope but the jitter is allowed to
// realize it is never read again at this point and remove the
// local from the root set. If you have the misfortune that you
// get a GC and finalize right now then the resource is deallocated
// by the time we get to:
SomeNativeCode.UseTheCachedHandle();
}
If you're in this unfortunate situation, use KeepAlive
to force a local to stay alive.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | Eric Lippert |