When GC.KeepAlive Doesn’t

The purpose of GC.KeepAlive(Object) is to tell the GC not to collect an object until a certain point. For example:

class MyObject
{
~MyObject()
{
Console.WriteLine(“MyObject Finalized”);
}             

   public static void Main()
{
MyObject obj = new MyObject();
LongRunningMethod();
GC.KeepAlive(obj); // ~MyObject will NOT be run before this call
...
}
}

KeepAlive will ensure ~MyObject will not get run before LongRunningMethod gets called. This is useful if the long running method passes the object out to unmanaged code, for example. In that case, you’ll want to keep the object from being collected by the GC until the method returns.

What’s the secret to KeepAlive? Nothing. It’s just a normal method with no side effects except holding a reference to an object. Since it holds a reference, the JIT considers the object rooted until that point (if no other reference to this object exists and if you are not in debuggable code), and the GC will not collect it.

So when does KeepAlive not keep an object alive? When it’s not called. Ok, that was deliberately cryptic, let me illustrate using the MyObject class above: 

public static void Main()
{
MyObject obj = new MyObject();
while (true)
{
GC.Collect(); // force a collection to illustrate
}
GC.KeepAlive(obj);
...
}

One would expect that when run, there would be nothing printed to the screen, since KeepAlive keeps obj from getting collected. But since KeepAlive is after a while(true) loop it’s actually unreachable. Compiling the code will give you compiler warning CS0162: Unreachable code detected on the GC.KeepAlive(obj) line. Since KeepAlive is not even being called, obviously it won’t hold the object live. However if you compile the code in debug mode, the JIT will extend lifetimes of references to the end of their enclosing method. In this case, KeepAlive actually isn’t necessary. But in the release case, a reference should be live until the last line of code that references it. So why is obj getting finalized?

Looking at the IL for Main, we see what happened:

.method public hidebysig static void Main() cil managed
{
.entrypoint
// Code size 21 (0x15)
.maxstack 1
.locals init ([0] class MyObject obj,
[1] bool CS$4$0000)
IL_0000: nop
IL_0001: newobj instance void MyObject::.ctor()
IL_0006: stloc.0
IL_0007: br.s IL_0011
IL_0009: nop
IL_000a: call void [mscorlib]System.GC::Collect()
IL_000f: nop
IL_0010: nop
IL_0011: ldc.i4.1
IL_0012: stloc.1
IL_0013: br.s IL_0009
} // end of method MyObject::Main

Since the code after the while loop is considered dead, the compiler has actually not built it. So the call to KeepAlive was optimized away, and is never actually called by the runtime. The JIT then thinks it is no longer reachable after the while loop, and the GC is free to collect it.