.NET Garbage Collector Part I : Fundamentals

Overview

The CLR .NET Garbage Collector is the automatic engine that efficiently manages memory allocating and releasing in our .NET applications. It’s based in a generational memory promoted feature that allows developers to delegate tasks related with memory management, thus .NET developers can focus in programming application funcionalities instead of memory deallocating questions. This was a great feature introduced as part of .NET Framework and its main goal is to locate objects at managed heap that are not longer reachable and reclaim the memory allocated by these objects

Nevertheless, although GC simplifies in high degree the memory manage, .NET developers are not completely exempts of some memory concerns and GC internals that is advisable to know for avoiding make some mistakes when coding and for building well-behaving programs for which performance is the key.

From an operational perspective, GC provides the following features:

  • Efficiently allocated objects on managed heap.
  • Implement a three-generational phases in its memory manage life-cycle
  • It´s the responsible for clearing the memory for dead objects and refresh it for allowing future allocations, compacting and re-ordering the memory slots for those objects that are survivors
  • Memory safety.
  • Partial garbage collections (for Gen0, Gen1,Gen2) enables the efficiently implementation of high concurrent applications like ASP.NET

In the most cases, .NET GC is triggered automatically by CLR when the system has achieved a specific low physical memory threshold or when the memory at managed head used by allocated objects overloads a specific bound. One can also manually invoke the GC by using the Collect() method. From a high level perspective when the GC is invoked then it reclaims memory allocated for dead objects, this process is also the responsible for compacting the survivor live objects moving them together and freeing the space corresponding to dead ones, this way the CG avoid as far as possible the memory fragmentation

Generations

The Garbage collector provides three different generations and all the garbage-collectable objects are allocated in a contiguous range of memory space, this way it´s possible to clear the non-reachable objects (garbage-marked) by looking at small part of the managed heap. These generations are in fact:

Generation 0: It’s the first memory generation space of GC. All short lived like temp local variables objects are stored in the segment corresponding to Gen0. In fact all new objects are initially stored here if and only if they are not greater that 85.000 bytes in which case are stored at LOH (Large Object Heap)

Generation 1: it’s the next generation level; objects that have survived to the first CG round are promoted and moved to generation 1 segment. Contains short-lived objects and acts as a buffer for the next GC round

Generation 2: It´s the older GC generation space, and contains the survivors of the previous GC rounds, that is to say the long-lived objects like static data, etc…

GC Phases

GC comprises a collection of phases that are guided by the following assumptions: The younger an object is the shorter its lifetime will be, the older objects tends to be even older, young objects tend to have strong relationship to each other and its access is usually at the same time and finally, compacting is faster over small heap sections that over the hole heap. With this assumptions in mind

The first stage is the marking phase in which all the live objects are localized and marked; later a relocating phase is the responsible for updating the references to objects that will be compacted. This compacting process is in charge of reclaims the memory occupied by dead objects and the compact the living ones. As a part of this process survivor objects will be moved at the end of the older segment (at this point it´s convenient to mention that Gen2 can use more than one memory segments, on the other hand Gen0 and Gen1 only implement a segment each of them that is commonly referred as ephemeral generation segment)

This way the live-objects from Gen2 that are promoted will be moved into an older segment, live-objects from Gen1 will be moved to a “more young” segment, this will be the base for internally establish the memory threshold at which the GC will be trigger.

This is not true for LOH objects, this objects are by default out of the compacting and moving actions in order to preserve the performance, however we can use the CSettings.LargeObjectHeapCompactionMode in order to compact the large object heap on demand

As I commented earlier, the generational feature of garbage collection improves the performance, when the heap memory threshold is achieved, GC can choose to manage only the objects at Generation 0 and can ignore the objects allocated in high level generations, if the memory collected from generation 0 is not enough then the CG algorithm can go to the next Generation level (Gen1), if this is not enough then GC will go to the Gen2.

How CG determines live objects

The initial CG assumption is that all the objects at heap are garbage and then CG (at the first marked-phase) constructs the graph of all objects that are reachable from application´s roots, all live objects will be part of graph. Properly speaking an application’s root (single root) is a storage location that contains a memory pointer to a reference type object, so this element points to an object at managed heap or is set to null. Only variables that are reference types are considered as root, value types fall out of this definition.

Inside this definition we can find:

  • Static fields defined inside a type or any method parameter
  • Any local variables or parameter object pointer on a thread’ stack
  • CPU registers
  • Pointers from Reachable Queue

So taking this into consideration, when the CC starts it assumes that thread’s stack doesn’t contain variables that points to live objects in the heap, no CPU registers refer to objects in the heap and no static fields refers to objects in the heap. This is when the CG run the marking phase in which the collector walks up the thread’s stack checking all the roots, if one root pointing to a live object is found then a bit-flag is activated inside the object’s sync-block index field, this is a recursive process, if a specific root directly refers to objects X and Z and X contains a X contains a field that refers to Y then Y will be also marked.

Once all live objects from an specific root are marked, CG will continue with the next root. CG is smart enough for avoiding duplicated marking the same object (and the down references from it) which enhance the performance and avoid infinite loops for circular object references.

All the marked objects are considered survivors (reachable by the application’s code), the unmarked ones are considered garbage and CG will reclaim the memory occupied by them, this is when compacting phase starts. In this phase the CG will transverse the heap looking for contiguous memory blocks of unmarked objects, if small blocks are found the collector leaves them alone, but if big ones are found them collector will rescue survivor contiguous objects from heap, releasing its memory in order to compact the heap and avoid fragmentation. Needless to say this has cost, moving objects in heap memory invalidates all CPU registers and variables that contains pointers to objects at heap, this is when the GC will review all the application’s root in order to update their pointers to the object’s new memory location, as one can image this is not a trivial task, GC is also responsible for updating deeper references to moved objects. Finally after the heap memory has been compacted, the common NextObjPtr pointer is positioned right after the last survivor object memory.

We will review in deep more about .NET CLR CG in later posts !

SotR