Garbage Collection in .NET

Garbage collection (GC) is an automatic memory management feature in the .NET Common Language Runtime (CLR). It identifies and frees up memory that is no longer being used by an application. This process helps prevent memory leaks and reduces the burden on developers to manually manage memory allocation and deallocation.

How Garbage Collection Works

The GC operates by tracking objects on the managed heap. When an object is no longer reachable by the application (i.e., no active references point to it), it is considered eligible for garbage collection. The GC then reclaims the memory occupied by these objects.

Generational Garbage Collection

The .NET GC employs a generational approach to optimize performance. It categorizes objects into different "generations" based on their age:

The GC primarily focuses on collecting Gen 0 because it contains the most ephemeral objects, leading to faster and more frequent collection cycles. When Gen 0 is full, a collection occurs. If many objects from Gen 0 survive, Gen 1 may also need to be collected. Similarly, if Gen 1 becomes full, Gen 2 is collected.

The GC Process Steps:

  1. Marking: The GC identifies all root objects (e.g., objects on the stack, static fields) that are still in use. It then traverses the object graph, marking all reachable objects.
  2. Relocation: The GC moves the surviving objects together on the heap. This process, known as "compaction," reduces fragmentation and creates contiguous blocks of free memory.
  3. Reclaiming: The memory occupied by unreachable (unmarked) objects is reclaimed and made available for new allocations.

GC Modes

The .NET GC can operate in different modes, affecting how and when it performs collections:

The default GC mode depends on the application type. You can configure the GC mode using the runtime configuration settings.

Controlling Garbage Collection

While the GC is automatic, there are scenarios where you might need to influence its behavior. The System.GC class provides methods for this:

Tip: Manually triggering garbage collection with GC.Collect() is rarely necessary and can often hurt performance more than it helps. Trust the automatic GC unless you have a very specific, measured reason to intervene.

Best Practices for Memory Management

Finalization

Objects can implement a finalizer (a method named with a tilde, e.g., ~MyClass()) to perform cleanup of unmanaged resources before they are garbage collected. However, finalization is generally a last resort due to its performance implications. Prefer the IDisposable pattern for deterministic resource cleanup.

Important: Finalizers can introduce overhead and make debugging more complex. Always try to use IDisposable for managing unmanaged resources.

Example of IDisposable and Finalizer:


using System;

public class MyResource : IDisposable
{
    private IntPtr unmanagedHandle;
    private bool disposed = false;

    public MyResource()
    {
        // Simulate acquiring an unmanaged resource
        unmanagedHandle = System.Runtime.InteropServices.Marshal.AllocHGlobal(100);
        Console.WriteLine("Resource acquired.");
    }

    // Implement IDisposable
    public void Dispose()
    {
        Dispose(true);
        // Suppress finalization since we've already cleaned up
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (!disposed)
        {
            if (disposing)
            {
                // Dispose managed resources here (if any)
                Console.WriteLine("Disposing managed resources.");
            }

            // Dispose unmanaged resources
            if (unmanagedHandle != IntPtr.Zero)
            {
                Console.WriteLine("Releasing unmanaged resource.");
                System.Runtime.InteropServices.Marshal.FreeHGlobal(unmanagedHandle);
                unmanagedHandle = IntPtr.Zero;
            }

            disposed = true;
        }
    }

    // Finalizer
    ~MyResource()
    {
        // Do not call Dispose(true) here. It's already been done or will be done by the GC.
        Dispose(false);
        Console.WriteLine("Finalizer called.");
    }
}
            

In this example, Dispose(true) is called when Dispose() is invoked directly (e.g., within a using statement), cleaning up both managed and unmanaged resources. When the GC calls the finalizer (because Dispose() was not called), Dispose(false) is called, which only cleans up unmanaged resources.

Note: The order in which finalizers are called is not guaranteed.