Bless Your .NET Object With a Shot of Lifeline

Anand Gupta
The Startup
Published in
5 min readJul 7, 2020
Illustration by Shruti Gupta

In my previous post, I discussed the eager root collection as an aggressive behavior of JIT (in Release mode / optimized code) to assist the garbage collector (GC), so that a object is not considered to be a root beyond the point of its usage. But sometimes we may want to extend the lifetime of an object beyond its usual lifetime. In this post, we will see an example of a scenario where we may want to extend the lifetime of an object. Also, we will look at how we can accomplish this in two ways — one is writing an API ourselves (in the spirit of learning by doing) and other is to use existing API in .NET.

Consider the below code to display the current time using a timer endlessly till we decide to exit the program:

Timer local variable — Debug vs. Release

Try running this under Debug mode, you will see an output as below. I hope you are not surprised by this result. You can see the output prints the time endlessly until we exit the program.

Time is 7:03 PM
Time is 7:03 PM
Time is 7:03 PM
Time is 7:03 PM
Time is 7:03 PM
Time is 7:03 PM
…….

Now try running the same code under Release mode.

The output when we run the same code under Release mode is nothing. The timer does not fire at all (or may only fire once or twice depending on the timing between execution garbage collection and timer firing).

Surprised! (or may be not if you know about eager root collection or have already read my previous blog on eager root collection)

This happens because of eager root collection. The timer object is not being used after line 8, hence when we initiate the garbage collection from line 9 to line 11, timer object is not reported as live roots by JIT, hence garbage collector does not mark it during mark phase and gets collected by it during sweep phase.

So, we have a situation where our code works as expected (timer fires endlessly till program exit) in Debug mode, but does not work (timer does not fire) at all in the Release mode. This is the quintessential “it works on my machine” moment!

The obvious question is what can I do about it? How can I ensure my code works as expected in both the Debug mode and Release mode? We like the idea of surprises (I know my wife does!), but we don’t like the unpleasant ones and most definitely don’t like it in production systems.

The solution is to add just a single line of code in our Main() method using the API in .NET that will give our humble timer object the much needed lifeline, so it can live longer and keep firing to show us the time till we want (but within the lifetime of the method execution). But before I come to that, let us try and write my own version of it - armed with our knowledge of eager root collection and method inlining.

We will design our API as a static class called Lifeline with a static method called KeepAlive().

Here is the full source code of my Lifeline API.

Lifeline API (this is our own API, not part of .NET Framework)

Yes, you seeing it right. The Lifeline.KeepAlive() method has absolutely no code in it. We are not using the obj argument we are being sent by the caller of this method. The key feature of this method is the attribute onthe method [MethodImpl(MethodImplOptions.NoInlining)]. This guarantees the JIT will never try to inline this method. What this means is, the call to this method will never be ‘inlined’, and a call to this method will definitely be made by passing the argument to its method parameter of type object (the design choice to have object type in the parameter is deliberate, so that this method can be invoked for any type of object).

Now we will invoke the Lifeline.KeepAlive() API method from the Main() method of our code as below and run it under Release mode (as well as Debug mode). Please note the call to Lifetime.KeepAlive() at line 14 by passing timer object as the argument :

Extend the lifetime of object using our own Lifeline.KeepAlive() API

Hurray! We could extend the lifetime of an object with a single small dose of Lifeline.KeepAlive().

Time is 7:45 PM
Time is 7:45 PM
Time is 7:45 PM
Time is 7:45 PM
Time is 7:45 PM
Time is 7:45 PM
…….

As promised, let us now discuss how we can accomplish this using existing .NET API. You can save yourself the effort of writing a Lifeline API as I did above and simply call GC.KeepAlive() provided in .NET. KeepAlive() is a static method on .NET framework’s (and .NET Core) GC class.

You might imagine GC.KeepAlive() must implement a very complicated and sophisticated logic — after all it is extending the lifetime of an object and messing with the default Release mode JIT behavior. But you may surprised to note, the implementation of GC.KeepAlive() is exactly identical to Lifeline.KeepAlive() we wrote — GC.KeepAlive() does not do anything, its method is completely empty and does nothing with the object we pass to it. It uses the power of [MethodImpl(MethodImplOptions.NoInlining)] to prevent JIT from inlining it. I encourage you to take a look at the implementation of GC.KeepAlive() in .NET framework source code.

We can change our code to use the GC.KeepAlive() .NET API to extend the lifetime of any object till the point of invocation of GC.KeepAlive(). Please note the call to the GC .KeepAlive() at line 14:

Extend the lifetime of an object using GC.KeepAlive() .NET API

The lifetime of the timer object has been extended till the point of invocation of GC.KeepAlive() on line 14.

Digging Deeper — for the more curious souls

We can use the WinDbg tool (in conjunction with SOS extension) to further examine and verify the JIT behavior and GC roots in Release mode with and without GC.KeepAlive().

If we run the code in Release mode, but without the GC.KeepAlive(), and examine the GCInfo (command !gcinfo on line 11), we will observe that there are no stack roots and also there are no instances of Timer in the heap (indicated by no System.Threading.Timer entry when we execute !dumpheap command on line 43).

WinDbg examination to verify JIT eager root collection behavior — timer object garbage collected

As discussed, when we add GC.KeepAlive(), the timer object will be kept alive. To verify this let us examine using WinDbg (and SOS.dll). We can observe there is a live root in stack at stack position rbp-40 as indicated by the result of !gcinfo on line 11. This can be further confirmed by !dumpheap command on line 53 that shows the presence of 1 object in the heap of type System.Threading.Timer (line 75)and by !gcroot command (line 83) that conclusively shows rb-40 stack location points to an object of type System.Threading.Timer (line 86 and line 87).

WinDbg examination to demonstrate effect of GC.KeepAlive() — timer object lifetime extended

Conclusion

The JIT has an aggressive behavior when we run our code in Release mode (optimized code). This leads to what is known as eager root collection. Objects may get collected by GC even though they may still appear to exist from lexical point of view. But if we want, we can extend the lifetime of objects. This can be done via GC.KeepAlive() API of .NET. We also discussed how we can write our own implementation of this API to better understand and appreciate how GC.KeepAlive() achieves its objective of extending an object’s lifetime in a surprisingly trivial manner.

--

--