Identify and eliminate bottlenecks in your application for optimized performance.
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.
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();
}
}
This section discusses some typical causes of memory leaks in C# programs.
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:
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:
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:
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:
private sealed class CachedItem
{
public object Value { get; set; }
public DateTime Expiration { get; set; }
}
private static readonly Dictionary<int, CachedItem> _cache = new Dictionary<int, CachedItem>();
public static void Add(int key, object value, TimeSpan lifespan)
{
_cache.Add(key, new CachedItem { Value = value, Expiration = DateTime.Now + lifespan });
}
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;
}
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:
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:
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.
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