Skip to main content

C# Memory Leaks

Introduction

Although C# is a managed language with automatic garbage collection, memory leaks can still occur. A memory leak happens when your application allocates memory that is never released, causing the application to consume more and more memory over time. This can lead to performance degradation and eventually crash your application when it runs out of memory.

Understanding memory leaks in C# is crucial for building efficient and stable applications. In this tutorial, we'll explore:

  • What causes memory leaks in C#
  • Common memory leak scenarios
  • How to identify memory leaks
  • Best practices to prevent them

What Causes Memory Leaks in C#?

In unmanaged languages like C++, memory leaks typically occur when you allocate memory but forget to free it. However, since C# has a garbage collector (GC), the causes are more subtle.

The most common cause of memory leaks in C# is unintentionally holding references to objects that are no longer needed. The garbage collector can only reclaim memory for objects that aren't referenced anywhere in your application.

Common Memory Leak Scenarios

1. Static References

Static fields and properties maintain references to objects for the entire lifetime of the application, preventing the garbage collector from reclaiming them.

csharp
public class MemoryLeakExample
{
// This static collection will never be garbage collected
private static List<byte[]> leakyCollection = new List<byte[]>();

public void CauseMemoryLeak()
{
// Each call adds 10MB to memory that won't be released
byte[] largeArray = new byte[10 * 1024 * 1024]; // 10MB array
leakyCollection.Add(largeArray);

Console.WriteLine($"Added 10MB to static collection. Current count: {leakyCollection.Count}");
}
}

If you call CauseMemoryLeak() in a loop, memory usage will grow continuously:

csharp
var example = new MemoryLeakExample();
for (int i = 0; i < 10; i++)
{
example.CauseMemoryLeak();
// Output will show increasing count
}

// Output:
// Added 10MB to static collection. Current count: 1
// Added 10MB to static collection. Current count: 2
// ...
// Added 10MB to static collection. Current count: 10

2. Event Handlers Not Unsubscribed

Forgetting to unsubscribe from events is one of the most common causes of memory leaks in C#.

csharp
public class Publisher
{
public event EventHandler SomeEvent;

public void RaiseEvent()
{
SomeEvent?.Invoke(this, EventArgs.Empty);
}
}

public class Subscriber : IDisposable
{
private readonly Publisher _publisher;

public Subscriber(Publisher publisher)
{
_publisher = publisher;
// Subscribe to the event
_publisher.SomeEvent += OnSomeEvent;
}

private void OnSomeEvent(object sender, EventArgs e)
{
Console.WriteLine("Event received!");
}

// If this method is not called, the event subscription creates a memory leak
public void Dispose()
{
// Unsubscribe from the event
_publisher.SomeEvent -= OnSomeEvent;
}
}

If you create a Subscriber object but don't call its Dispose() method, the publisher will maintain a reference to the subscriber, preventing it from being garbage collected.

3. Improper IDisposable Implementation

Not properly implementing IDisposable for classes that use unmanaged resources can cause memory leaks.

csharp
public class ResourceHolder : IDisposable
{
// Unmanaged resource
private FileStream _fileStream;

public ResourceHolder(string path)
{
_fileStream = new FileStream(path, FileMode.Open);
}

// This method must be called to prevent memory leaks
public void Dispose()
{
if (_fileStream != null)
{
_fileStream.Dispose();
_fileStream = null;
}
}
}

4. Cached Objects Never Cleared

Implementing caching mechanisms without proper eviction policies can lead to memory leaks.

csharp
public class ImageCache
{
// This dictionary will grow unbounded if entries are never removed
private static Dictionary<string, byte[]> _imageCache = new Dictionary<string, byte[]>();

public byte[] GetImage(string key)
{
if (_imageCache.TryGetValue(key, out byte[] image))
{
return image;
}

// Simulate loading an image
byte[] loadedImage = new byte[1024 * 1024]; // 1MB
_imageCache[key] = loadedImage;

return loadedImage;
}

// Missing: A method to clear old cache entries!
}

Identifying Memory Leaks

Tools to Identify Memory Leaks

  1. Visual Studio Diagnostics Tools:

    • Performance Profiler
    • Memory Usage tool
  2. Third-party tools:

    • JetBrains dotMemory
    • ANTS Memory Profiler
    • PerfView

Simple Way to Monitor Memory Usage

You can use this simple code to monitor memory usage in your application:

csharp
public static void MonitorMemoryUsage()
{
while (true)
{
// Force garbage collection
GC.Collect();
GC.WaitForPendingFinalizers();

// Get memory usage after collection
long memoryUsed = GC.GetTotalMemory(true) / (1024 * 1024); // MB

Console.WriteLine($"Memory used: {memoryUsed} MB");
Thread.Sleep(5000); // Check every 5 seconds
}
}

Best Practices to Prevent Memory Leaks

1. Dispose Objects Properly

Always dispose objects that implement IDisposable either explicitly or using using statements.

csharp
// Method 1: using statement
using (var resource = new ResourceHolder("path/to/file.txt"))
{
// Use the resource here
} // Dispose is automatically called when exiting the block

// Method 2: try-finally
ResourceHolder resource = null;
try
{
resource = new ResourceHolder("path/to/file.txt");
// Use the resource here
}
finally
{
resource?.Dispose();
}

2. Unsubscribe from Events

Always unsubscribe from events when they're no longer needed.

csharp
public class ProperSubscriber : IDisposable
{
private readonly Publisher _publisher;

public ProperSubscriber(Publisher publisher)
{
_publisher = publisher;
_publisher.SomeEvent += OnSomeEvent;
}

private void OnSomeEvent(object sender, EventArgs e)
{
Console.WriteLine("Event handled properly!");
}

public void Dispose()
{
// Always unsubscribe
_publisher.SomeEvent -= OnSomeEvent;
}
}

3. Use Weak References

For caching scenarios, consider using WeakReference to allow the GC to collect objects when needed:

csharp
public class WeakCache<TKey, TValue> where TValue : class
{
private Dictionary<TKey, WeakReference<TValue>> _cache = new Dictionary<TKey, WeakReference<TValue>>();

public void Add(TKey key, TValue value)
{
_cache[key] = new WeakReference<TValue>(value);
}

public bool TryGetValue(TKey key, out TValue value)
{
value = null;

if (_cache.TryGetValue(key, out WeakReference<TValue> weakRef))
{
if (weakRef.TryGetTarget(out value))
{
return true;
}
else
{
// The object was garbage collected, remove the entry
_cache.Remove(key);
}
}

return false;
}
}

4. Implement Proper Caching Strategies

Use time-based or size-based eviction policies for caches:

csharp
public class BoundedCache<TKey, TValue>
{
private readonly int _maxItems;
private readonly Dictionary<TKey, TValue> _cache = new Dictionary<TKey, TValue>();
private readonly Queue<TKey> _accessOrder = new Queue<TKey>();

public BoundedCache(int maxItems)
{
_maxItems = maxItems;
}

public void Add(TKey key, TValue value)
{
if (_cache.Count >= _maxItems && !_cache.ContainsKey(key))
{
// Evict oldest item
TKey oldestKey = _accessOrder.Dequeue();
_cache.Remove(oldestKey);
}

_cache[key] = value;
_accessOrder.Enqueue(key);
}

public bool TryGetValue(TKey key, out TValue value)
{
return _cache.TryGetValue(key, out value);
}
}

5. Avoid Capturing Variables in Lambdas

Be cautious with lambdas and closures, as they can capture and hold references to objects:

csharp
public class LambdaExample
{
public Action CreatePotentialLeak()
{
// This array is 10MB
byte[] largeArray = new byte[10 * 1024 * 1024];

// This lambda captures largeArray, preventing it from being garbage collected
return () =>
{
Console.WriteLine($"Array length: {largeArray.Length}");
};
}

public Action CreateBetterAction()
{
// Copy just what's needed to avoid capturing the entire object
int length = new byte[10 * 1024 * 1024].Length;

// This lambda only captures the length (4 bytes) instead of the entire array
return () =>
{
Console.WriteLine($"Array length: {length}");
};
}
}

Real-World Example: Memory Leak in a Web Application

Here's a common scenario in ASP.NET web applications where memory leaks can occur:

csharp
// In a static class providing application-wide services
public static class GlobalCache
{
// This cache grows unbounded as new users log in
private static Dictionary<string, UserSession> _userSessions = new Dictionary<string, UserSession>();

public static void StoreUserSession(string userId, UserSession session)
{
_userSessions[userId] = session;
}

public static UserSession GetUserSession(string userId)
{
if (_userSessions.TryGetValue(userId, out UserSession session))
{
return session;
}
return null;
}

// Missing: A method to remove old sessions!
}

// User session that might contain large amounts of data
public class UserSession
{
public string UserId { get; set; }
public DateTime LastActivity { get; set; }
public Dictionary<string, object> SessionData { get; set; } = new Dictionary<string, object>();
}

Fixing the Web Application Memory Leak

csharp
public static class ImprovedGlobalCache
{
// Add time-based expiration
private static Dictionary<string, CachedSession> _userSessions = new Dictionary<string, CachedSession>();
private static Timer _cleanupTimer;

static ImprovedGlobalCache()
{
// Cleanup every 10 minutes
_cleanupTimer = new Timer(CleanupOldSessions, null, TimeSpan.Zero, TimeSpan.FromMinutes(10));
}

private static void CleanupOldSessions(object state)
{
DateTime expirationThreshold = DateTime.UtcNow.AddHours(-1); // 1-hour timeout

// Find expired sessions
var expiredKeys = _userSessions
.Where(kvp => kvp.Value.LastActivity < expirationThreshold)
.Select(kvp => kvp.Key)
.ToList();

// Remove expired sessions
foreach (var key in expiredKeys)
{
_userSessions.Remove(key);
}

Console.WriteLine($"Cache cleanup: Removed {expiredKeys.Count} expired sessions. Remaining: {_userSessions.Count}");
}

public static void StoreUserSession(string userId, UserSession session)
{
_userSessions[userId] = new CachedSession
{
Session = session,
LastActivity = DateTime.UtcNow
};
}

public static UserSession GetUserSession(string userId)
{
if (_userSessions.TryGetValue(userId, out CachedSession cachedSession))
{
// Update last activity
cachedSession.LastActivity = DateTime.UtcNow;
return cachedSession.Session;
}
return null;
}

// Explicitly remove a session
public static void RemoveSession(string userId)
{
_userSessions.Remove(userId);
}

private class CachedSession
{
public UserSession Session { get; set; }
public DateTime LastActivity { get; set; }
}
}

Summary

Memory leaks in C# occur when objects remain referenced even when they're no longer needed, preventing the garbage collector from reclaiming the memory. The most common causes include:

  1. Static references that live for the entire application lifetime
  2. Event handlers that aren't unsubscribed
  3. Improper implementation of IDisposable
  4. Unbounded caches without eviction policies
  5. Capturing unnecessary variables in lambda expressions

To prevent memory leaks:

  • Always dispose objects that implement IDisposable
  • Unsubscribe from events when objects are disposed
  • Use weak references for caching scenarios
  • Implement proper caching strategies with eviction policies
  • Be mindful of variable capturing in lambdas
  • Regularly profile your application for memory usage

By following these best practices, you can build C# applications that efficiently manage memory and remain stable over extended periods of use.

Additional Resources

Exercises

  1. Create a simple console application that demonstrates a memory leak using a static collection, then fix it.
  2. Implement a cache with a time-based eviction policy.
  3. Fix a memory leak caused by an unsubscribed event handler.
  4. Use Visual Studio's memory profiler to identify memory leaks in a sample application.
  5. Create a class that properly implements the IDisposable pattern with both managed and unmanaged resources.


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)