Garbage Collector

Learn about memory allocation and deallocation in .NET.

Introduction

There’s a significant difference in how value and reference types are stored in memory. When we create a value-type variable, the value behind the variable is stored on the method stack. When the method returns (finishes running), the stack is cleared. Reference-type variables are also stored on the stack, but the variable contains the memory address of the region in the heap where the object is actually stored.

When a method finishes running, reference-type variables are also immediately deleted, but the objects these variables were pointing to aren’t cleared instantly. They’re deleted automatically some time later by the garbage collector.

Garbage collector

When there are no references pointing to an object, this object is considered an orphan. It’s the garbage collector’s job to clear memory areas that aren’t referenced by the application. The garbage collector doesn’t run every time a reference is deleted from the stack. The CLR launches the garbage collector only when necessary (for instance, when the app is running out of memory).

Objects in the heap aren’t stored in a sequential manner. There might be gaps between objects and the heap is heavily fragmented. For optimization purposes, a compaction follows each garbage collection, where remaining objects are moved to a contiguous block of memory. References update with new memory addresses so that links between variables and objects don’t break.

Notes on memory

For objects larger than 85000 bytes, there’s a special type of heap called Large Object Heap. Its difference from the regular heap is that there’s no compaction in the Large Object Heap. Moving large objects in memory is time-consuming and might jeopardize performance.

Compaction takes time, and the application pauses until the process is complete. However, benefits obtained from memory optimization outweigh the potential disadvantages. In order to reduce its footprint on performance, the garbage collector doesn’t scan the whole application memory every time it needs to collect orphan objects. Different areas of memory are scanned at different time intervals based on the generation:

  • Generation 0 holds newly created objects, except for those whose size is larger than 85000 bytes. The garbage collector begins its scan in this area of memory. Generation 0 objects that survive the collection are automatically moved to Generation 1.
  • Generation 1 is only scanned if there’s still memory needed after the garbage collector finishes scanning Generation 0. Objects that survive the scan are moved to Generation 2.
  • Generation 2 is for objects whose lifespan is longest in the app. Garbage collectors rarely scan this part of memory. This area is only scanned when there’s still memory needed after Generation 1 was scanned.

The GC class

The garbage collector’s functionality is exposed to .NET developers through the System.GC class. We can send commands to the garbage collector through its static methods. Most often, GC is never used, because there isn’t any need: garbage collection is automatic for managed objects in .NET.

Note: Managed objects are all objects that were created within the context of the CLR.

We might call the garbage collector manually in some case. If our code works with unmanaged resources, for example. Also, if our code constantly creates and uses large objects, we might want to call the garbage collector manually so that Generation 1 and 2 objects are cleared more often.

Here are some of the methods of System.GC:

  • AddMemoryPressure(): This informs the CLR that a large chunk of memory was allocated for an unmanaged resource. This information enables CLR to better determine when it’s time to launch garbage collection.

Collect(): This launches the garbage collection process. There are multiple overloads that allow us to control what generations must be scanned.

  • GetGeneration(object): This shows what generation an object belongs to.

  • GetTotalMemory(): This returns the amount of memory, in bytes, that’s currently in use in the managed heap.

  • WaitForPendingFinalizers(): This blocks the current thread until all objects that are being collected are cleared from memory.

Let’s discuss the Collect() method in greater detail. There are overloads that allow us to finetune the collection process. For instance, we could indicate which generations must be scanned:

GC.Collect(1); // Collects orphan objects from Generation 0 and 1
GC.Collect(2); // Collects orphan objects from Generation 0, 1, and 2

We can also pass the GCCollectionMode enum, which will change when the garbage collection process begins:

  • GCCollectionMode.Forced: This starts the collection process immediately.
  • GCCollectionMode.Optimized: This lets the CLR decide the best time to launch garbage collection.

Example:

GC.Collect(0, GCCollectionMode.Forced); // Collects Generation 0 orphan objects immediately

Example

To see how the garbage collector works, let’s look at a simple example. Suppose we create several objects and check their generations. We expect them to be zero because we’ve just created these objects. Then, we remove references from some of them, call the garbage collector, and check the generations of those who survived the garbage collection. Generation must now be 1.

Get hands-on with 1200+ tech skills courses.