Mastering Garbage Collection in C#: Understanding Generations, Finalization, and Optimization…
Mastering Garbage Collection in C#: Understanding Generations, Finalization, and Optimization Techniques In C#, the garbage collector (GC) is a core part of...
Mastering Garbage Collection in C#: Understanding Generations, Finalization, and Optimization Techniques

In C#, the garbage collector (GC) is a core part of the runtime that manages memory allocation and deallocation for managed objects. Understanding how garbage collection works can help you write more efficient code and reduce memory-related issues. As part of my learning journey so far reading Pro C# 10 with .NET 6 by Andrew Troelsen and Phil Japikse, In this post, we’ll dive into the concept of generational garbage collection, finalization, and how you can optimize your code for better memory management.
The Generational Model of Garbage Collection
C# uses a generational garbage collection model with three generations:
- Generation 0: Short-lived objects, typically allocated on the stack. This is where newly created objects begin their lifecycle.
- Generation 1: Objects that survive the first garbage collection sweep. These are considered moderately long-lived.
- Generation 2: Long-lived objects that have survived multiple garbage collection sweeps. These objects are often global or static.
The garbage collector assumes that most objects will be short-lived, so it prioritizes collecting from Generation 0 first. If an object survives a collection, it is promoted to the next generation.
The following code snippet is just for illustration purposes, you will likely never use System.GC.Collect() in your code as this will be called automatically for you.
class Program
{
static void Main()
{
CreateShortLivedObject();
GC.Collect(); // Triggering garbage collection manually (not recommended in production)
// Force a garbage collection and wait for
// each object to be finalized.
// With this approach, you can rest assured that all finalizable objects,
// have had a chance to perform any necessary cleanup before your program continues
GC.WaitForPendingFinalizers();
Console.ReadLine();
}
static void CreateShortLivedObject()
{
var obj = new object(); // Short-lived object in Generation 0
}
}In the example above, obj is a short-lived object in Generation 0. After the method CreateShortLivedObject() finishes execution, the object is eligible for garbage collection. However, it may not be collected immediately if there's no memory pressure or a GC cycle isn't triggered. GC.WaitForPendingFinalizers() will suspend the calling thread during the collection process. This is a good thing, as it ensures your code does not invoke methods on an object currently being destroyed!
Graduation of Objects: Objects that survive a garbage collection cycle are promoted to a higher generation (i.e., from Generation 0 to 1, and from Generation 1 to 2). The higher the generation, the less frequently the garbage collector will attempt to reclaim the memory occupied by that object.
Finalization and Object Destruction
Finalization occurs when objects need to perform some cleanup before being reclaimed by the garbage collector. The Finalize() method is used to release unmanaged resources, but it has a cost. If an object overrides Finalize(), it is placed in a queue after the first GC sweep, and a second pass is required to finalize and collect it.
class MyFinalizationClass
{
~MyFinalizationClass()
{
// Cleanup code here
// note other object reference
Console.WriteLine("Cleaning up!!");
}
}In the rare case that you do build a C# class that uses unmanaged resources, you will obviously want to ensure that the underlying memory is released in a predictable manner. Suppose you have created a new C# Console Application project named SimpleFinalize and inserted a class named MyFinalizationClass that uses an unmanaged resource (whatever that might be) and you want to override Finalize(). The odd thing about doing so in C# is that you can’t do it using the expected override keyword.
C# finalizers look similar to constructors, in that they are named identically to the class they are defined within. In addition, finalizers are prefixed with a tilde symbol (~). Unlike a constructor, however, a finalizer never takes an access modifier (they are implicitly protected), never takes parameters, and can’t be overloaded (only one finalizer per class).
“A real-world finalizer would do nothing more than free any unmanaged resources and would not interact with other managed objects, even those referenced by the current object, as you can’t assume they are still alive at the point the garbage collector invokes your Finalize() method.”
After an object with a finalizer is marked for finalization, it is promoted to an unreachable state but will not be collected until the finalizer runs. This makes finalizable objects more costly to collect.
Garbage Collection Triggering: Why Objects Might Still Exist in Memory
It’s important to note that garbage collection in C# does not occur immediately after an object goes out of scope. The GC is designed to run when certain conditions are met, such as when the system runs low on memory or when a significant amount of memory has been allocated.
Here’s an example demonstrating that even after a function exits, the object might still be in memory:
class Program
{
static void Main()
{
CreateObject();
// GC hasn't been triggered yet, so object might still be in memory
}
static void CreateObject()
{
var myObject = new MyClass(); // Reference type object on the heap
Console.WriteLine("Object created.");
}
}In this case, even though myObject goes out of scope after CreateObject() finishes executing, the garbage collector might not reclaim it immediately. This is because the GC runs non-deterministically, often based on memory usage patterns and not simply when objects go out of scope.
Lazy Initialization with System.Lazy<T>
To avoid creating objects prematurely and potentially overwhelming the garbage collector with unused objects, you can utilize lazy initialization. System.Lazy<T> ensures that an object is only created when it’s actually needed.
class Program
{
static Lazy lazyObject = new Lazy(() => new MyClass());
static void Main()
{
// Object is created only when accessed for the first time
var obj = lazyObject.Value;
Console.WriteLine("Lazy object created.");
}
}In this example, MyClass will only be instantiated when lazyObject.Value is accessed, reducing the number of objects in memory and the workload of the garbage collector.
Best Practices for Optimizing Garbage Collection
Here are some tips to help minimize the impact of garbage collection in your application:
- Minimize Object Lifetimes: Prefer short-lived objects that go out of scope quickly, so they can be reclaimed in Generation 0.
- Use Structs When Possible: Structs are value types and allocated on the stack rather than the heap, avoiding garbage collection.
struct MyStruct
{
public int Value;
}3. Avoid Finalizers When Possible: Finalizers add extra overhead to garbage collection. If you need to release unmanaged resources, consider implementing the IDisposable pattern instead.
class MyClass : IDisposable
{
public void Dispose()
{
// Release resources here
}
}4. Use Object Pooling: Reuse objects instead of constantly creating and discarding them. This is especially useful for objects that are expensive to create.
class ObjectPool
{
private readonly Stack pool = new Stack();
public MyClass GetObject()
{
return pool.Count > 0 ? pool.Pop() : new MyClass();
}
public void ReturnObject(MyClass obj)
{
pool.Push(obj);
}
}5. Optimize Large Object Heap (LOH) Usage: Large objects (typically 85,000 bytes or more) are allocated in a special memory area called the LOH. LOH collections are more expensive, so minimize allocations to the LOH when possible by breaking down large objects into smaller ones.
6. Prefer Arrays Over Collections: Arrays can be more memory-efficient than collections like List<T> or Dictionary<TKey, TValue>, especially when you know the size ahead of time.
Conclusion
Garbage collection is a vital part of memory management in C#. Understanding how it works can help you write more efficient and robust applications. By leveraging techniques such as lazy initialization, minimizing object lifetimes, and using memory-efficient data structures, you can reduce the frequency and impact of garbage collection on your application’s performance.
With these tips, you’ll be well on your way to optimizing your applications for garbage collection. Keep in mind that understanding how the runtime manages memory is key to writing efficient and performant C# code.