Loading...

Memory Management and the Garbage Collector in C#

Learn memory management and the garbage collector in C# to understand object lifetime, allocation, and cleanup processes.

In .NET applications, Memory Management is handled automatically by the Garbage Collector (GC). Developers do not need to manually manage the lifecycle of objects. The GC detects unused objects and frees them from memory. However, understanding how memory management works is crucial for performance optimization.


What Is Memory Management?

Memory management is the process of tracking the lifecycle of objects created by an application in RAM. In the .NET environment, there are two types of memory areas:


Difference Between Stack and Heap

PropertyStackHeap
Memory AreaSmall and fastLarge, managed area
Stored TypesValue typesReference types
ManagementAutomatic (scope-based)Managed by the Garbage Collector
LifecycleCleared when the method endsRemains until detected by the GC
Access SpeedVery fastSlower

What Is the Garbage Collector (GC)?

The Garbage Collector automatically cleans up unused objects on the heap. This reduces the risk of memory leaks. The GC kicks in at intervals during program execution and releases objects that are no longer referenced.


using System;

class Program
{
    static void Main()
    {
        for (int i = 0; i < 1000; i++)
        {
            var data = new byte[1024 * 1024]; // 1 MB
        }

        Console.WriteLine("Before memory cleanup...");
        GC.Collect(); // Manual GC invocation
        Console.WriteLine("After memory cleanup...");
    }
}

Manually invoking the GC is generally not recommended and should be done only in special scenarios (e.g., tests or after memory-intensive operations).


How Does the GC Work?

The Garbage Collector uses a generation-based collection approach. This means short-lived objects and long-lived objects are kept in different areas.

The GC not only targets short-lived objects but scans the entire heap to free objects that are unreachable from root references.


Difference Between Dispose and Finalize

GC alone is not sufficient for memory management. Unmanaged resources (e.g., files, network connections, GDI objects) are not cleaned up automatically by the GC. For such objects, use the IDisposable interface and the Finalize (destructor) method.


class FileResource : IDisposable
{
    private bool disposed = false;

    public void Write(string data)
    {
        if (disposed)
            throw new ObjectDisposedException(nameof(FileResource));
        Console.WriteLine($"Writing data: {data}");
    }

    public void Dispose()
    {
        if (!disposed)
        {
            Console.WriteLine("Resource released (Dispose).");
            disposed = true;
            GC.SuppressFinalize(this);
        }
    }

    ~FileResource()
    {
        Console.WriteLine("Finalize called.");
    }
}

// Usage:
using (var file = new FileResource())
{
    file.Write("Test");
}

The Dispose() method is used for manual cleanup, while Finalize (destructor) is called by the GC. SuppressFinalize() prevents the GC from calling the destructor again.


What Is a Memory Leak?

Although the GC is automatic, memory leaks can still occur if developers mismanage resources. In particular, when large event handlers or static references are not properly cleared, objects cannot be reclaimed.


// Classic memory leak example:
class Worker
{
    public event EventHandler DataReady;
}

class Program
{
    static Worker w = new Worker();

    static void Main()
    {
        w.DataReady += (s, e) => Console.WriteLine("Event remained subscribed!");
        w = null; // GC cannot collect because the event handler still holds a reference!
    }
}

In such cases, events should be unsubscribed manually with -=.


GC Modes and Performance Settings

.NET GC has two main operating modes:


// Example in app.config or runtimeconfig.json:
<configuration>
  <runtime>
    <gcServer enabled="true"/>
  </runtime>
</configuration>

The GC.TryStartNoGCRegion() method can prevent the GC from running for a certain period. This ensures uninterrupted operation in real-time applications.


Monitoring GC Events

You can use methods like GCNotification and GC.GetTotalMemory() to track when the GC runs or how much memory it has collected.


using System;

class Program
{
    static void Main()
    {
        long before = GC.GetTotalMemory(false);
        var data = new byte[10_000_000];
        long after = GC.GetTotalMemory(false);

        Console.WriteLine($"Difference: {(after - before) / 1024 / 1024} MB");

        GC.RegisterForFullGCNotification(10, 10);
        GC.Collect();
        Console.WriteLine("GC triggered!");
    }
}

Example: Large Data Processing

In applications working with large datasets, creating unnecessary objects increases GC load. The example below demonstrates object reuse using ArrayPool<T> for memory optimization.


using System;
using System.Buffers;

class Program
{
    static void Main()
    {
        var pool = ArrayPool<byte>.Shared;
        byte[] buffer = pool.Rent(1024 * 1024); // Rent a 1 MB buffer

        for (int i = 0; i < buffer.Length; i++)
            buffer[i] = 255;

        Console.WriteLine("Data processed.");
        pool.Return(buffer); // Does not create GC pressure
    }
}

ArrayPool enables reuse of frequently used objects and reduces GC pressure.


Performance and Best Practices


TL;DR

  • The Garbage Collector automatically cleans up unused objects on the heap.
  • The stack holds fast, short-lived value types; the heap stores reference types.
  • The GC operates using a generation model (0, 1, 2).
  • IDisposable provides manual cleanup for unmanaged resources.
  • Memory leaks often stem from events or static references.
  • Use object pools (ArrayPool, ObjectPool) for better performance.

Related Articles