Troubleshooting .NET memory leaks – Part 2: Eliminating memory leaks

Note: This is the second part of a series on troubleshooting .NET memory leaks. Please read the first article if you haven’t already.

In the first part of this series, you learned how memory leaks occur when a program occupies memory but fails to release it when closed. Opening and closing a program without rebooting causes it to consume more memory, leading to slower program execution, increased CPU use, and potential crashes or other problems.

The first article also explored various tools for analyzing and diagnosing memory consumption in .NET apps. It stressed the importance of regularly monitoring application memory use to diagnose and fix memory leaks in .NET applications.

This article describes some of the common sources of memory leaks in .NET applications and demonstrates how to fix them in code.

Fixing memory leaks

The first article used a simple app that intentionally contained a memory leak. The C# program creates an instance of the MyClass class inside an infinite loop in the Main method. The MyClass constructor initializes an array of integers with a specified size but doesn’t release the memory when the object is no longer needed. The ~MyClass method is a destructor that the garbage collector calls when collecting objects but it doesn’t release the memory either. The resulting memory leak occurs because the program continuously allocates new memory without freeing the previously allocated memory.

To solve the memory leak in this program, you must release the memory allocated by the MyClass objects when no longer needed. One way to do this is to implement the IDisposable interface and provide a Dispose method that releases the resources used by objects.

Here’s how to modify the code from the first article to implement IDisposable and free the memory:

class MyClass : IDisposable 
{
private int[] data;

public MyClass(int size)
{
data = new int[size];
}
public void Dispose()
{
data = null;
GC.SuppressFinalize(this);
}

~MyClass()
{
Console.WriteLine("Destructor called");
Dispose();
}
}

Common memory leak causes in C# programs

This section discusses some typical causes of memory leaks in C# programs.

Improperly disposing of objects

The following sample C# code demonstrates how improperly disposing of unmanaged objects properly can lead to memory leaks.

To begin, open Visual Studio and create a new console application with the code below:

using System.Runtime.InteropServices; 
namespace MemoryLeakExample
{
class Program
{
static void Main(string[] args)
{
while (true)
{
var myObj = new MyClass();
Thread.Sleep(100);
}
}
}
class MyClass
{
private readonly IntPtr _bufferPtr;
public MyClass()
{
_bufferPtr = Marshal.AllocHGlobal(4 * 1024 * 1024); // 4 MB
}
}
}

This C# program has a loop that creates a new MyClass object every 100 milliseconds. MyClass has a field called bufferPtr that uses the AllocHGlobal method from the Marshal class to allocate 4 MB of memory each time it creates a new object.

The program runs indefinitely and causes a memory leak because it doesn’t release the allocated memory after creating each object. Over time, the program continues to consume memory until it crashes.

To fix this memory leak, you need to release the memory allocated by each MyClass object after it’s no longer needed. Start by implementing a method in MyClass that releases the allocated memory using the FreeHGlobal method from the Marshal class. Then, call this method before destroying the MyClass object as follows:

class Program 
{
static void Main(string[] args)
{
while (true)
{
var myObj = new MyClass();
var myObj = new MyClass();
myObj.Dispose();
Thread.Sleep(100);
}
}
}

class MyClass : IDisposable
{
private readonly IntPtr _bufferPtr;
private bool _disposed = false;

public MyClass()
{
_bufferPtr = Marshal.AllocHGlobal(4 * 1024 * 1024); // 4 MB
}

protected virtual void Dispose(bool disposing)
{
if (_disposed)
return;

if (disposing)
{
// Free any other managed objects here.
}

// Free any unmanaged objects here.
Marshal.FreeHGlobal(_bufferPtr);
_disposed = true;
}

public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}

~MyClass()
{
Dispose(false);
}
}

In C#, improperly disposing of objects can lead to memory leaks, resource depletion, and poor performance. Here’s how to avoid these problems:

  • Use the using statement —This C# language feature helps automatically dispose of objects that implement the IDisposable interface. The using statement ensures that the Dispose method of the object is called when executed using a statement.
  • Use the Dispose pattern — If you’re implementing a class that uses unmanaged resources, you should implement Dispose. This pattern calls the Dispose method when the object is no longer needed.
  • Use managed objects whenever possible — The garbage collector automatically disposes of them when they’re no longer needed, helping avoid resource leaks and performance issues.

Keeping references to objects unnecessarily

Keeping references to objects unnecessarily in C# programs can lead to memory leaks and slow performance, as demonstrated by the following code:

using System; 
using System.Collections.Generic;

namespace MemoryLeakExample
{
class Program
{
static void Main(string[] args)
{
var myList = new List();
while (true)
{
// populate list with 1000 integers
for (int i = 0; i < 1000; i++)
{
myList.Add(new Product(Guid.NewGuid().ToString(), i));
}
// do something with the list object
Console.WriteLine(myList.Count);
}
}
}

class Product
{
public Product(string sku, decimal price)
{
SKU = sku;
Price = price;
}

public string SKU { get; set; }
public decimal Price { get; set; }
}
}

The C# program creates a list of integers called myList and then enters an infinite loop. Within the loop, it adds 1,000 integers to myList using a for loop and prints the current count of myList to the console. The program continues to add integers to the list, printing the count indefinitely.

This program demonstrates a potential memory leak because the list continuously grows but doesn’t remove any items. As the program continues to run, the list consumes memory until the system runs out of available memory, crashing the program.

Removing objects or data that are no longer needed is essential to preventing memory leaks. In this program, add a line of code after printing the list count that removes the first 1,000 integers from the list:

while (true) 
{
// populate list with 1000 integers
for (int i = 0; i < 1000; i++)
{
myList.Add(new Product(Guid.NewGuid().ToString(), i));
}
// do something with the list object
Console.WriteLine(myList.Count);
// clear the list and set its reference to null
myList.Clear();
}

By clearing the contents of the list object and setting its reference to null, you ensure that the old contents of the list object are properly released and their memory is freed up. Setting the reference to null also prevents the program from keeping unnecessary references to the list object, which can cause memory leaks.

Here are some best practices to avoid keeping references to objects unnecessarily:

  • Avoid static variables — Static variables are shared with all instances of a class. They can prevent objects from being garbage collected if they hold references to those objects, slowing down performance. Instead, use instance variables.
  • Use local variables — Local variables are declared within a method. They’re automatically garbage collected when the method exits.
  • Use the using statement — This statement helps ensure that objects are disposed of when they’re no longer needed.
  • Use the IDisposable interface — This action is especially useful if your class uses unmanaged resources. The interface requires implementing the Dispose method to release unmanaged resources.

Incorrect use of event handlers

Here’s a C# code block demonstrating how incorrect use of event handlers can lead to memory leaks:

using System; 
namespace MemoryLeakExample
{
class Program
{
static void Main(string[] args)
{
var publisher = new EventPublisher();

while (true)
{
var subscriber = new EventSubscriber(publisher);
// do something with the publisher and subscriber objects
}
}

class EventPublisher
{
public event EventHandler MyEvent;

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

class EventSubscriber
{
public EventSubscriber(EventPublisher publisher)
{
publisher.MyEvent += OnMyEvent;
}

private void OnMyEvent(object sender, EventArgs e)
{
Console.WriteLine("MyEvent raised");
}
}
}
}

In this code, an EventPublisher object creates an event called MyEvent. An EventSubscriber object subscribes to this event in its constructor by attaching a handler method called OnMyEvent to the MyEvent event. However, the program never detaches the OnMyEvent method from the event, which causes the EventSubscriber object to stay alive even when it’s no longer needed.

To fix this issue, detach the OnMyEvent method from the event in the EventSubscriber object’s destructor:

class EventSubscriber : IDisposable 
{
private readonly EventPublisher _publisher;
private bool _disposed = false;
public EventSubscriber(EventPublisher publisher)
{
_publisher = publisher;
_publisher.MyEvent += OnMyEvent;
}

private void OnMyEvent(object sender, EventArgs e)
{
Console.WriteLine("MyEvent raised");
}
protected virtual void Dispose(bool disposing)
{
if (_disposed)
return;

if (disposing)
{
_publisher.MyEvent -= OnMyEvent;
}

_disposed = true;
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}

Now modify the while loop in the Main method to dispose of the subscriber object:

while (true) 
{
var subscriber = new EventSubscriber(publisher);
// do something with the publisher and subscriber objects
subscriber.Dispose();
}

By detaching the OnMyEvent method from the event in the EventSubscriber object’s destructor, you release the old instance of the EventSubscriber object, freeing up the memory. This method prevents the program from keeping unnecessary references to the EventSubscriber object, which can lead to memory leaks.

The incorrect use of event handlers in C# programs can lead to leaks, performance problems, and unexpected behavior. Here are some best practices to avoid them:

  • Unsubscribe from events when you no longer need them — Failing to unsubscribe from events can lead to memory leaks and performance lags. When an object subscribes to an event, it holds a reference to the event source. If the event source is no longer needed, unsubscribe the object from the event to allow the event source to be garbage collected.
  • Use weak event handlers — Weak event handlers don’t hold strong references to the event source. This helps prevent memory leaks and allows the event source to be garbage collected when no longer needed.
  • Avoid using static event handlers — Static event handlers are associated with a static class or a static field. They can prevent objects from being garbage collected if they hold references to those objects. Instead, consider using instance event handlers.

Improper caching

Improper caching can cause memory leaks in a C# program because objects that are cached and no longer needed can remain in memory, consuming resources and leading to the exhaustion of available memory over time. This process happens when the cache is implemented without considering the objects’ lifespan.

Here is an example of a C# program that uses a cache but does not properly manage its lifespan. (We are using a rudimentary caching system, as the options available on the market do not lead to memory leaks):

using System; 
using System.Collections.Generic;
class Cache
{
private static Dictionary<int, object> _cache = new Dictionary<int, object>();

public static void Add(int key, object value)
{
_cache.Add(key, value);
}

public static object Get(int key)
{
return _cache[key];
}
}

class Program
{
static void Main(string[] args)
{
for (int i = 0; i < 1000000; i++)
{
Cache.Add(i, new object());
}

Console.WriteLine("Cache populated");

Console.ReadLine();
}
}

This program creates a cache with one million objects and adds them to the cache. However, since the cache is never cleared, all one million objects remain in memory even after they are no longer needed.

To solve those memory leaks, you must implement the cache needs in a way that allows for the lifespan of cached objects to be managed. You can use a cache that automatically removes objects after a certain period or when the cache becomes too large. Here is an example of how to implement it:

  • Add a CachedItem class that contains the cached object and its expiration time.
    private sealed class CachedItem 
    {
    public object Value { get; set; }
    public DateTime Expiration { get; set; }
    }
  • Inside the Cache class, instead of using a Dictionary<int, object> to store the cached items, we need to use a Dictionary<int, CachedItem> type to store the cached items with additional information such as expiration time.
    private static readonly Dictionary<int, CachedItem> _cache =  new Dictionary<int, CachedItem>();
                      
  • Modify the Add method to store the cached item as a CachedItem with a specified lifespan.
    public static void Add(int key, object value, TimeSpan lifespan) 
    {
    _cache.Add(key, new CachedItem { Value = value, Expiration = DateTime.Now + lifespan });
    }
  • Modify the Get method to check the expiration time of the cached item and remove it from the cache if it has expired.
    public static object? Get(int key) 
    {
    if (!_cache.ContainsKey(key))
    {
    return null;
    }

    CachedItem item = _cache[key];

    if (item.Expiration < DateTime.Now)
    {
    _cache.Remove(key);
    return null;
    }
    return item.Value;
    }
  • Inside the Program class, modify the call to the Cache.Add method to pass the expiration argument:
    Cache.Add(i, new object(), TimeSpan.FromMinutes(10));
                      

In this modified program, we create a cache with one million objects and add them to the cache with a lifespan of 10 minutes using the Add method. The Get method checks the expiration time of each cached object and removes it from the cache if it has expired. This ensures that objects no longer needed are appropriately removed from memory, preventing memory leaks.

Here are some best practices for avoiding improper caching in C# programs:

  • Understand the lifespan of cached objects: Before caching an object, consider its lifespan and how long it should remain in memory. You should also determine whether it's appropriate to cache the object at all or if it would be better to recreate it each time it's needed.
  • Use expiration policies: When caching an object, you should set an expiration time or policy so that it can be removed from the cache automatically when it’s no longer needed. You can use built-in expiration policies in C# caching frameworks — such as the MemoryCache class in the System.Runtime.Caching namespace — or create custom expiration policies.
  • Monitor cache size: You should monitor the size of the cache and ensure that it doesn't grow too large. Large caches can cause performance issues and memory leaks, mainly if the cache contains many large objects. You can set a maximum cache size and implement eviction policies to ensure the cache stays within a specific size.

Large object graphs

To visualize how large object graphs can lead to memory leaks, copy the following code to a Console application within Visual Studio:

using System; 
using System.Collections.Generic;
namespace MemoryLeakExample
{
class Program
{
static void Main(string[] args)
{
var rootNode = new TreeNode();
while (true)
{
// create a new subtree of 10000 nodes
var newNode = new TreeNode();
for (int i = 0; i < 10000; i++)
{
var childNode = new TreeNode();
newNode.AddChild(childNode);
}
rootNode.AddChild(newNode);
}
}
}

class TreeNode
{
private readonly List<TreeNode> _children = new List<TreeNode>();
public void AddChild(TreeNode child)
{
_children.Add(child);
}
}
}

This code creates a TreeNode object as the root node of a tree structure. The program then repeatedly creates new subtrees of 10,000 nodes and adds them as children of the root node. However, the program never removes the old subtrees, so it continues using up memory.

To fix this issue, modify the TreeNode class to expose the child nodes as public property, then create a RemoveChildAt method:

class TreeNode 
{
private readonly List<TreeNode> _children = new List<TreeNode>();
public IReadOnlyList<TreeNode> Children => _children;
public void AddChild(TreeNode child)
{
_children.Add(child);
}
public void RemoveChildAt(int index)
{
_children.RemoveAt(index);
}
}

Now modify the while loop within the Main method to remove the old subtrees after you’re done using them:

while (true) 
{
// create a new subtree of 10000 nodes
var newNode = new TreeNode();
for (int i = 0; i < 10000; i++)
{
var childNode = new TreeNode();
newNode.AddChild(childNode);
}
rootNode.AddChild(newNode);
// remove the old subtrees to free up memory
if (rootNode.Children.Count > 10)
{
rootNode.RemoveChildAt(0);
}
}

By removing the old subtrees, you ensure that the program doesn’t use up too much memory. You also prevent memory leaks by removing the old subtrees so the program doesn’t keep unnecessary references to old objects.

Here are some best practices for avoiding large object graphs in C# programs:

  • Use lazy loading — Lazy loading is a technique where objects aren’t loaded until needed, improving performance.
  • Use weak references — Weak references don’t prevent objects from garbage collection. They also help avoid memory leaks.
  • Avoid circular references — Circular references occur when objects reference each other. Avoiding circular references can also help improve performance.

Conclusion

This article concludes the troubleshooting .NET memory leaks series. It provides working code samples containing some common sources of memory leaks and how to fix them in code. It explains each memory leak case and some best practices for avoiding them.

Fixing memory leaks in .NET applications requires identifying the root cause, reviewing code for incorrect object handling, optimizing object creation and destruction, using a garbage collector, and monitoring memory use. Follow these steps to reduce the likelihood of memory leaks and improve the performance and stability of your application.

Was this article helpful?
Monitor your applications with ease

Identify and eliminate bottlenecks in your application for optimized performance.

Related Articles

Write For Us

Write for Site24x7 is a special writing program that supports writers who create content for Site24x7 "Learn" portal. Get paid for your writing.

Write For Us

Write for Site24x7 is a special writing program that supports writers who create content for Site24x7 “Learn” portal. Get paid for your writing.

Apply Now
Write For Us