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:
- Stack: Used for value types (
int,bool,struct, etc.). It’s fast and automatically released. - Heap: Used for reference types (
class,string,array, etc.). It’s managed by the GC.
Difference Between Stack and Heap
| Property | Stack | Heap |
|---|---|---|
| Memory Area | Small and fast | Large, managed area |
| Stored Types | Value types | Reference types |
| Management | Automatic (scope-based) | Managed by the Garbage Collector |
| Lifecycle | Cleared when the method ends | Remains until detected by the GC |
| Access Speed | Very fast | Slower |
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.
- Gen 0 (Generation 0): Newly created objects. The most frequently collected area.
- Gen 1: Objects that survived Gen 0. Used for medium-lived objects.
- Gen 2: Long-lived objects (e.g., singletons, caches, static data).
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:
- Workstation GC: Optimized for desktop applications (single user, UI prioritized).
- Server GC: Optimized for parallel collection on multi-core servers.
// 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
- Avoid calling the GC manually; .NET handles it according to its own scheduling.
- Always clean up unmanaged resources using
Disposeorusing. - Avoid large object allocations (
>85KB); the Large Object Heap (LOH) is collected more slowly. - Don’t forget to unsubscribe events (
-=); otherwise the GC cannot collect them. - Use object pools (
ArrayPool,ObjectPool) instead of creating many small, repetitive objects.
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
IDisposable and the Using Pattern in C#
Learn IDisposable and the using pattern in C# to manage resources safely, release unmanaged objects, and avoid leaks.
Performance Optimization with Span<T> and Memory<T> in C#
Learn performance optimization in C# using Span<T> and Memory<T> to manage memory efficiently and process data faster.
Unsafe Code and Pointers in C#
Learn unsafe code and pointers in C# to work with memory addresses, low-level operations, and advanced performance scenarios.