Memory Management in C# — Complete Guide
In this tutorial, you will learn about Memory Management in C#. We cover key concepts, practical examples, and best practices to help you master this topic.
Hook
C# manages memory automatically through Garbage Collection, but understanding how the GC works is essential for building high-performance applications. Proper resource management with IDisposable and IAsyncDisposable prevents memory leaks and ensures efficient use of system resources in your .NET applications.
Learning Path
graph LR A[Memory Management] --> B[GC Fundamentals] A --> C[IDisposable] A --> D[Finalizers] B --> E[Generations] B --> F[LOH] C --> G[IAsyncDisposable] style A fill:#4a90d9,color:#fff style B fill:#4a90d9,color:#fff style C fill:#4a90d9,color:#fff style D fill:#4a90d9,color:#fff style E fill:#4a90d9,color:#fff style F fill:#4a90d9,color:#fff style G fill:#4a90d9,color:#fff
Garbage Collection Fundamentals
The .NET GC manages memory in three generations.
// Gen 0: Short-lived objects (temporary variables)
// Gen 1: Objects that survived one collection
// Gen 2: Long-lived objects (application singletons)
// Large Object Heap (LOH): Objects >= 85,000 bytes
public class GcDemo
{
public static void ShowGenerations()
{
var obj = new byte[1000];
Console.WriteLine($"Gen: {GC.GetGeneration(obj)}"); // 0
GC.Collect(0);
Console.WriteLine($"After collect: {GC.GetGeneration(obj)}"); // 1
GC.Collect(1);
Console.WriteLine($"After Gen 1: {GC.GetGeneration(obj)}"); // 2
Console.WriteLine($"Total memory: {GC.GetTotalMemory(false):N0} bytes");
Console.WriteLine($"Collections: Gen0={GC.CollectionCount(0)}, " +
$"Gen1={GC.CollectionCount(1)}, Gen2={GC.CollectionCount(2)}");
}
}
IDisposable
Unmanaged resources (file handles, database connections, sockets) require explicit cleanup.
public class FileManager : IDisposable
{
private FileStream? _stream;
private bool _disposed;
public FileManager(string path)
{
_stream = File.OpenRead(path);
Console.WriteLine("Resource acquired");
}
public void ReadData()
{
if (_disposed)
throw new ObjectDisposedException(nameof(FileManager));
// Read from stream
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (_disposed) return;
if (disposing)
{
// Dispose managed resources
_stream?.Dispose();
_stream = null;
Console.WriteLine("Managed resources released");
}
// Free unmanaged resources (if any)
_disposed = true;
}
~FileManager()
{
Dispose(false); // Finalizer only cleans unmanaged resources
}
}
// Usage
using (var manager = new FileManager("test.txt"))
{
manager.ReadData();
} // Dispose called automatically
The Dispose Pattern
The full dispose pattern handles inheritance correctly.
public abstract class ResourceBase : IDisposable
{
private bool _disposed;
private IntPtr _unmanagedResource; // Example only
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (_disposed) return;
if (disposing)
{
// Clean up managed resources
}
// Clean up unmanaged resources
if (_unmanagedResource != IntPtr.Zero)
{
Marshal.FreeHGlobal(_unmanagedResource);
_unmanagedResource = IntPtr.Zero;
}
_disposed = true;
}
~ResourceBase() => Dispose(false);
}
public class DerivedResource : ResourceBase
{
private FileStream? _stream;
protected override void Dispose(bool disposing)
{
if (disposing)
{
_stream?.Dispose();
}
base.Dispose(disposing);
}
}
IAsyncDisposable
For async cleanup operations (closing network connections, flushing streams).
public class AsyncDatabaseConnection : IAsyncDisposable
{
private SqlConnection? _connection;
private bool _disposed;
public async Task ExecuteAsync()
{
await _connection!.OpenAsync();
// Execute command
}
public async ValueTask DisposeAsync()
{
if (_disposed) return;
if (_connection != null)
{
await _connection.CloseAsync();
await _connection.DisposeAsync();
_connection = null;
}
_disposed = true;
}
}
// Usage
await using (var db = new AsyncDatabaseConnection())
{
await db.ExecuteAsync();
}
Weak References
Use weak references for caches that should not prevent garbage collection.
public class WeakCache<TKey, TValue> where TValue : class
{
private readonly Dictionary<TKey, WeakReference<TValue>> _cache = new();
public void Add(TKey key, TValue value)
{
_cache[key] = new WeakReference<TValue>(value);
}
public bool TryGet(TKey key, out TValue? value)
{
if (_cache.TryGetValue(key, out var weakRef))
{
return weakRef.TryGetTarget(out value);
}
value = null;
return false;
}
}
GC Modes and Configuration
// Configure GC in .csproj or runtimeconfig.json
/*
<PropertyGroup>
<ServerGarbageCollection>true</ServerGarbageCollection>
<ConcurrentGarbageCollection>true</ConcurrentGarbageCollection>
</PropertyGroup>
*/
// Programmatic GC hints (use sparingly)
GC.Collect(2, GCCollectionMode.Optimized);
GC.WaitForPendingFinalizers();
Common Mistakes
Forgetting to call Dispose: Always use
usingstatements forIDisposableobjects. Resource leaks degrade performance.Finalizers running long operations: Finalizers run on a single thread. Blocking or long operations in finalizers freeze the finalizer queue.
Large object heap fragmentation: Frequent allocations of objects near 85KB create LOH fragmentation. Use array pooling or object reuse.
Calling GC.Collect manually: The GC is self-tuning. Manual calls usually harm performance. Only use in specific scenarios (testing, post-startup).
Not implementing the dispose pattern correctly: Forgetting
GC.SuppressFinalize(this)or not calling base class dispose in derived classes.
Practice Questions
Implement a thread-safe Connection Pool that properly disposes connections when released.
Create a wrapper class for
HttpClientthat implementsIAsyncDisposable.Write a memory-efficient cache using
WeakReference<T>that allows the GC to reclaim entries.Challenge: Build a custom memory pool using
System.Buffers.MemoryPool<T>that limits total memory usage across the application.
FAQ
Mini Project: Resource Pool
Build a generic resource pool with proper disposal.
using System.Collections.Concurrent;
public class ResourcePool<T> : IDisposable where T : IDisposable
{
private readonly ConcurrentBag<T> _resources;
private readonly Func<T> _factory;
private readonly int _maxSize;
private bool _disposed;
public ResourcePool(Func<T> factory, int maxSize = 10)
{
_factory = factory;
_maxSize = maxSize;
_resources = new ConcurrentBag<T>();
}
public T Get()
{
ObjectDisposedException.ThrowIf(_disposed, this);
if (_resources.TryTake(out T? resource))
{
Console.WriteLine("Reusing pooled resource");
return resource;
}
Console.WriteLine("Creating new resource");
return _factory();
}
public void Return(T resource)
{
if (_disposed)
{
resource.Dispose();
return;
}
if (_resources.Count < _maxSize)
{
_resources.Add(resource);
Console.WriteLine("Resource returned to pool");
}
else
{
resource.Dispose();
Console.WriteLine("Pool full, resource disposed");
}
}
public void Dispose()
{
if (_disposed) return;
_disposed = true;
while (_resources.TryTake(out T? resource))
{
resource.Dispose();
}
Console.WriteLine($"Pool disposed, remaining resources cleaned");
}
}
// Usage
var pool = new ResourcePool<MemoryStream>(() => new MemoryStream(), maxSize: 3);
var stream = pool.Get();
// Use stream
stream.WriteByte(0x42);
pool.Return(stream);
var reused = pool.Get(); // Gets pooled instance
Console.WriteLine($"Position: {reused.Position}"); // 0 (stream was reset)
pool.Dispose();
Output:
Creating new resource
Resource returned to pool
Reusing pooled resource
Position: 1
Pool disposed, remaining resources cleaned
Understanding memory management in C# is crucial for building reliable, high-performance applications. The .NET GC handles most memory automatically, but proper use of IDisposable, IAsyncDisposable, and awareness of GC behavior will prevent resource leaks and optimize your application's memory footprint.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro