C# Garbage Collector

C# Garbage Collector

All you need to know

·

6 min read

Goal: Provide the ultimate guide for C# memory management and garbage collection to improve your coding skills and interview readiness.

C# is a "managed" programming language: no extra code is needed to free the memory allocated for objects when they're no longer in use, thanks to the garbage collector. Before diving into how the garbage collector works, understanding how C# manages memory is necessary.

C# memory management

C# uses two different memory management strategies to hold declared variables.

The Stack

With a Last-In-First-Out logic, the stack holds the data of declared value types such as Int, Bool, Struct, etc... These types are pushed in the Stack on declaration and popped at the end of the current scope.

The Heap

The heap holds all reference types: objects. All reference types are derived from the object class. However, to know where exactly the object data is stored in the heap, its reference (or address) is kept in the Stack (this mechanism is similar to pointers in C++).

The diagram above explains how C# reference types are allocated in memory

While writing this article, I discovered SharpLab which is a tool that helps with Stack and Heap visualization (continue reading about sharplab here).

Variable types as parameters

Copies of value types are passed instead of the original variable which explains why any changes to a value type parameter are not reflected outside the method. As a workaround, you can force passing the value type as reference type using ref keyword. For reference types, since we are passing around the reference and not the object itself any modification is directly reflected on the original object.

The diagram above explains how C# value types are stored in the Stack and when they are removed from it

The image above illustrates the state of the Stack regarding how value types are created and deleted in the code.

The output of the code in the example is:

4/2 = 2

Resetting number to 0.

Current value of a: 4.

Passing a as a parameter doesn't affect its value because value-type parameters are copies of the original variables.

Object lifecycle

Only the Heap is concerned with garbage collection. As explained above, variables in the Stack are deleted automatically when exiting the scope in which they are declared.

How does the GC work?

The GC is an advanced feature of C#, there is no way and no point to describe how it works in detail, but here is what you need to know:

Generational garbage collection

In a C# application, objects have different life cycles. The GC classifies objects into 3 generations:

  • Gen0 for short-lived objects, objects usually start at this generation.

  • Gen1 for average lived objects. When the garbage collection cycle starts and identifies objects in Gen0 that are still in use, it promotes them to Gen1.

  • Gen2 for long-lived objects, that survived two cycles of garbage collection. With one exception, large objects are directly created as part of Gen2.

GC activation

The GC activates when memory usage exceeds a certain threshold, there are different thresholds for every generation. Thresholds change during runtime in response to application behavior. Also:

  • If the threshold of Gen1 is reached, the garbage collection sweeps Gen1 and Gen0

  • If the threshold of Gen2 is reached, the garbage collection sweeps all generations.

Object discovery

The GC needs to know what objects are used in an application, this is referred to as object discovery. The GC analyses the objects and their dependencies to create a dependency tree, static variables are where it starts.

Unused object detection

The GC leverages the stack to identify unused objects: any object present in the heap whose reference is no longer in the stack gets marked for deletion.

GC performance

Thread freeze

The GC is a great feature, but it comes with a catch: whenever garbage collection starts, all the application threads are frozen. This becomes an issue when low latency must be achieved.

Limitations

The GC is inept when it comes to cleaning unmanaged objects such as disk files, database connections, and network sockets...

GC and best practices

The GC does a good job at cleaning memory. However, to achieve better performance and optimal memory usage, it is necessary to adopt a couple of good practices.

Implementing IDisposable

Whenever a class uses unmanaged objects, having a correct implementation of IDisposable is necessary. Continue reading here.

Another advantage of implementing IDisposable is using syntactic sugar:

using (A a = new A())
{
    // Write code that uses "a" here
}

It is called "syntactic sugar" because it is simpler than writing:

A a = null;
try
{ 
    a = new A();
    // Write code that uses "a" here
}
finally
{
    a.Dispose();
}

Under the hood, using is replaced with try/finally:a class implementing IDisoposable is instantiated in the try block and disposed of in the finally block (even if an exception is thrown, Dispose() is always executed).

Avoid unnecessary boxing

Boxing refers to wrapping a value type in an object, which means that it's the GC's responsibility. This is not very impactful for small applications but as an application evolves, it becomes challenging to judge how many times the boxing operation is executed and the impact it has on the GC.

Not using structs

Structs are very similar to classes but are value types. Sometimes it makes more sense to use them instead of classes to hold data while avoiding Heap allocations. Continue reading here.

Resolving memory issues

Sometimes, developers face issues regarding applications' memory usage.

Stack overflow and x64 architecture

Stack overflow exceptions happen when the Stack can't take any more data. As mentioned above, reference types need the stack to store the references and can be responsible for this issue. Using an x64 architecture may remedy this issue thanks to the large memory address.

Memory leaks and Profiling tools

Memory leaks happen when the GC fails to mark unused objects for garbage collection. To narrow the investigation down, a memory profiling tool can be helpful: it will provide a detailed report about the objects used at runtime and how much memory is allocated to them. Visual Studio ships with a profiling tool that gets the job done, but JetBrains dotMemory is the developers' choice.

Weak references

Sometimes, objects "resist" garbage collection (especially when working with WinForms and WPF). Using a WeakReference helps alleviate the problem because it breaks easily and allows the GC to collect the object.

GC.Collect()

Yes, garbage collection operations can be triggered manually but it is just for extreme cases as a temporary fix. Using GC.Collect() is not a "C# best practice". Remember, garbage collection freezes all threads to perform this job.

Affecting null to references

This practice is widely used by C# developers and by affecting null, the object data in the heap loses its reference in the stack and therefore can be marked for garbage collection. However, it is not needed in well-written code as explained in this stack overflow question.

Closing thoughts

Garbage Collection in C# is a double-edged sword: it's a powerful feature that takes care of memory management for developers but "with great power, comes great responsibilities"! Developers need to be aware of C# managed memory and adopt best practices to avoid any potential blowback when pushing new code to production.

Writing articles about advanced topics takes a lot of time and effort. I would appreciate it if you provide your feedback by adding a Reaction, Commenting and/or subscribing to my newsletter. Thanks in advance!

Did you find this article valuable?

Support Simplified Software Engineering by becoming a sponsor. Any amount is appreciated!