Object lifetime in C#

Avinash Karat
6 min readJun 5, 2021

If you are really passionate about C#, then you should always aim to go a level deeper than your peers and make an effort to learn the internals of the language. Having some idea about the object life time will be a good start. Knowing these kind of things will make you a better C# programmer in the long run.

Here it goes….

In our typical application, we create objects of classes, and do all of our activities. After we done with our objects, what we know that the garbage collector will take care of the cleaning up activities. But do you know exactly what happens during this phase?

Let’s see

Assume you have a method in your Program class that allocates a local Car object as follows:

static void MakeACar() {

Car myCar = new Car();

}

Notice that this Car reference (myCar) has been created directly within the MakeACar() method and has not been passed outside of the defining scope (via a return value or ref/out parameters). Thus, once this method call completes, the myCar reference is no longer reachable, and the associated Car object is now a candidate for garbage collection. Understand, however, that you can’t guarantee that this object will be reclaimed from memory immediately after MakeACar() has completed. All you can assume at
this point is that when the CLR performs the next garbage collection, the myCar object could be safely destroyed.

We all know that objects are stored in the managed heap. The the managed heap is more than just a random chunk of memory accessed by the CLR. The .NET garbage collector will compact empty blocks of memory inside the managed heap(when necessary) for the purposes of optimization.

the new object() instruction tells the CLR to perform the following core operations:

1. Calculate the total amount of memory required for the object to be allocated (including the memory required by the data members and the base classes).

2. Examine the managed heap to ensure that there is indeed enough room to host the object to be allocated. If there is, the specified constructor is called, and the caller is ultimately returned a reference to the new object in memory, whose address just happens to be identical to the last position of the next object pointer.

3. Finally, before returning the reference to the caller, advance the next object
pointer to point to the next available slot on the managed heap.

As your application is busy allocating objects, the space on the managed heap may eventually become full. When processing the newobj instruction, if the CLR determines that the managed heap does not have sufficient memory to allocate the requested type, it will perform a garbage collection in an attempt to free up memory. Thus, the next rule of garbage collection is also quite simple.

The Role of Application Roots

In order to further dive into this topic, we need to understand what an application root is. Simply put, a root is a storage location containing a reference to an object on the managed heap. Strictly speaking, a root can fall into any of the following categories:

  • References to global objects (though these are not allowed in C#, CIL code does permit allocation of global objects)
  • References to any static objects/static fields
  • References to local objects within an application’s codebase
  • References to object parameters passed into a method
  • References to objects waiting to be finalized (described later in this chapter)
  • Any CPU register that references an object

During a garbage collection process, the runtime will investigate objects on the managed heap to determine whether they are still reachable (i.e., rooted) by the application. To do so, the CLR will build an object graph, which represents each reachable object on the heap. The object graphs are used to document all reachable objects. As well, be aware that the garbage collector will never graph the same object twice, thus avoiding the nasty circular reference count found in COM programming.

Assume the managed heap contains a set of objects named A, B, C, D, E, F, and G. During garbage collection, these objects (as well as any internal object references they may contain) are examined for active roots. After the graph has been constructed, unreachable objects (which you can assume are objects C and F) are marked as garbage.

After objects have been marked for termination (C and F in this case — as they are not accounted for in the object graph), they are swept from memory. At this point, the remaining space on the heap is compacted, which in turn causes the CLR to modify the set of active application roots (and the underlying pointers) to refer to the correct memory location (this is done automatically and transparently). Last but not least, the next object pointer is readjusted to point to the next available slot.

Understanding Object Generations

When the CLR is attempting to locate unreachable objects, it does not literally examine every object placed on the managed heap. Doing so, obviously, would involve considerable time, especially in larger (i.e., real-world) applications.

To help optimize the process, each object on the heap is assigned to a specific “generation.” The idea behind generations is simple: the longer an object has existed on the heap, the more likely it is to stay there. For example, the class that defined the main window of a desktop application will be in memory until the program terminates. Conversely, objects that have only recently been placed on the heap (such as an object allocated within a method scope) are likely to be unreachable rather quickly. Given these assumptions, each
object on the heap belongs to one of the following generations:

  • Generation 0: Identifies a newly allocated object that has never been marked for collection
  • Generation 1: Identifies an object that has survived a garbage collection (i.e., it was marked for collection but was not removed because the sufficient heap space was acquired)
  • Generation 2: Identifies an object that has survived more than one sweep of the garbage collector

The garbage collector will investigate all generation 0 objects first. If marking and sweeping (or said more plainly, getting rid of ) these objects results in the required amount of free memory, any surviving objects are promoted to generation 1. Below the picture, we can see how a set of surviving generation 0 objects (A, B, and E) are promoted once the required memory has been reclaimed.

If all generation 0 objects have been evaluated but additional memory is still required, generation 1 objects are then investigated for reachability and collected accordingly. Surviving generation 1 objects are then promoted to generation 2. If the garbage collector still requires additional memory, generation 2 objects are evaluated. At this point, if a generation 2 object survives a garbage collection, it remains a generation 2 object, given the predefined upper limit of object generations.

The bottom line is that by assigning a generational value to objects on the heap, newer objects (such as local variables) will be removed quickly, while older objects (such as a program’s main window) are not “bothered” as often.

Conclusion:

That’s it. Hope you have got an overall picture of the object lifetime in C#. Armed with this knowledge, go deeper and deeper in C#, to understand its real beauty.

Happy learning!

--

--

Avinash Karat

Working professionally as a full stack .Net developer . Also have a keen interest in personal productivity, meditation&personal finance. Here to share things.