Below is a very simple console app (try the fiddle):
using System;
using System.Threading;
using System.Threading.Tasks;
public class ConsoleApp
{
class Callback
{
public Callback() { }
~Callback() { Console.WriteLine("~Callback"); }
}
static void Test(CancellationToken token)
{
Callback callback = new Callback();
while (true)
{
token.ThrowIfCancellationRequested();
// for the GC
GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced);
GC.WaitForPendingFinalizers();
Thread.Sleep(100);
}
// no need for KeepAlive?
// GC.KeepAlive(callback);
}
public static void Main()
{
var cts = new CancellationTokenSource(3000);
try
{
Test(cts.Token);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced, true);
GC.WaitForPendingFinalizers();
Console.WriteLine("Enter to exit...");
Console.ReadLine();
}
}
Here, the callback
object doesn't get garbage-collected until it goes out of scope of the Test
method.
I thought GC.KeepAlive(callback)
would be required to keep it alive inside Test
(as MSDN suggests), but apparently it is not (commented out in the code above).
Now if I change the code like below, the callback
does get garbage-collected, as expected:
Callback callback = new Callback();
callback = null;
This happens with .NET 4.5.1.
The question: Am I missing something? Can I rely upon this behavior, or is it something .NET version specific?
@Porges' comments explain everything very well:
Try building & running it in Release mode, without the debugger attached. I get the expected behaviour there but not in Debug.
...
ie. running with Ctrl-F5, not just F5. It collects it instantly for me in each of .NET 4/4.5/4.5.1. But yes, you can't really rely on this behaviour.
The Release build and Ctrl-F5 brought back the expected behavior. I urge @Porges to post this as an answer, which I'd up-vote and accept with thanks.
As a follow-up, I'd like to feature the following interesting behavior. Now with Release + Ctrl-F5, even if I un-comment the // GC.KeepAlive(callback)
line in my code, the callback
still gets garbage-collected. Apparently, this is because the compiler recognizes this line as unreachable due to while (true)
loop and still doesn't emit a strong reference on callback
.
The following is the correct pattern:
static void Test(CancellationToken token)
{
Callback callback = new Callback();
try
{
while (true)
{
token.ThrowIfCancellationRequested();
// for the GC
GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced);
GC.WaitForPendingFinalizers();
Thread.Sleep(100);
}
}
finally
{
GC.KeepAlive(callback);
}
}
It's also interesting to look at the GC.KeepAlive
implementation:
[MethodImpl(MethodImplOptions.NoInlining)]
public static void KeepAlive(object obj)
{
}
As expected, it does nothing and merely servers as a hint to the compiler to generate IL code which keeps a strong reference to the object, up to the point where KeepAlive
is called. MethodImplOptions.NoInlining
is very relevant here to prevent any optimizations like above.
.NET garbage collection is non-deterministic.
The MSDN page to which you linked says it all really - emphasis added:
The purpose of the KeepAlive method is to ensure the existence of a reference to an object that is at risk of being prematurely reclaimed by the garbage collector.
Just because callback
can be garbage collected before it goes out of scope on return from Test
does not mean that it will be.