In this tutorial, we’ll see how you can use dotMemory to locate and fix memory leaks in your apps. But before moving on, let’s agree on what a memory leak is.
What Is a Memory Leak?
According to Wikipedia, a memory leak is a result of incorrect memory management when “an object is stored in memory but cannot be accessed by the running code.” In addition, “memory leaks add up over time, and if they are not cleaned up, the system eventually runs out of memory.”
Actually, if we’ll strictly follow the definition above, “classic” memory leaks are impossible in .NET apps. Garbage Collector (GC) fully controls memory release and removes all objects that cannot be accessed by the code. Moreover, after the app is closed, GC entirely frees the memory occupied by the app. Nevertheless, point #2 (memory exhaustion because of a leak) is quite real. Of course, this won’t crash the system, but sooner or later the app will raise an OutOfMemory exception.
Why can this happen? The thing is, GC collects only unreferenced objects. If there’s a reference to an object you don’t know about, GC won’t collect the object. Therefore, the main tactic in fixing memory leaks is to determine objects that add up over time (causing the leaks) as well as the objects that retain the former ones in memory.
Let’s try this tactic for fixing a leak in our sample application.
Once again, the app we’ll use for our tutorial is Conway’s Game of Life. Please download and unpack the archive before proceeding any further.
Let’s assume we want to return some money spent on the Game of Life development and decide to add a couple of windows* that show various ads to users. Following worst practices, we show our ad windows every time a user starts Game of Life (clicks the Start button). When a user clicks on a banner, he/she is redirected to some website and the ad window is closed (the user may also close the window using the standard close button, though that’s not what we really want). To change ads, the ad windows use a timer (based on the
You can see the implementation of the
AdWindow class in the AdWindow.cs file.
So, the feature is added and now is the best time to test it. Let’s run dotMemory and ensure that the ad window doesn't affect the app’s memory usage (in other words, it is correctly allocated and collected).
Step 1. Run Profiler
- Open the Game of Life solution in Visual Studio.
- Run dotMemory using the menu ReSharper | Profile | Profile Startup Project (Memory).
This will open the Profiler Configuration window.
- In the Profiler Configuration window, turn on Start collecting allocation data immediately. This will tell dotMemory to start collecting profiling data right after the app is launched.
Here’s what the window should look like after you specify the options:
- Click Run to start the profiling session. This will run our app and open the main Analysis page in dotMemory.
Step 2. Get Snapshots
Once the app is running, we can get a memory snapshot. As we want to test our new ad windows and how they affect memory usage, we’ll need to take two snapshots: one right after the windows are displayed (we’ll use this snapshot as a basis for comparison), and another after the ad windows are closed. The second snapshot is needed to ensure that GC removes our windows from memory.
- Start the game by clicking the Start button in the app. The ad windows will appear.
- Click the Get Snapshot button in dotMemory.
This will capture the data and add the snapshot to the snapshot area. Getting a snapshot doesn’t interrupt the profiling process, thus allowing us to get another snapshot.
- Close the ad windows in our app.
- Get a snapshot one more time by clicking the Get Snapshot button in dotMemory.
- End the profiling session by closing the Game of Life app.
The main page now contains two snapshots.
Step 3. Compare Snapshots
Now, we’ll compare and contrast the two collected snapshots. What do we want to see? If everything works fine, the ad windows should be present in the first snapshot but absent in the second. Let’s take a look.
- Click Add to comparison for each snapshot to add them to the comparison area. The order in which you add snapshots is not important as dotMemory always uses the older snapshot as the basis for comparison.
- Click Compare in the comparison area. This will open the Comparison view.
The view shows how many objects of a certain class were created (the New objects column) and removed (the Dead objects column) between snapshots. Survived objects shows how many objects have survived garbage collection, or, in other words, exist in both snapshots.
Currently, we’re interested in the
- To ease the finding of the
AdWindowclass, let’s sort all objects by the namespace they belong to. To do this, click Group by Namespace in the list of views.
- Open the
What’s that? Two
GameOfLife.AdWindowobjects are in the Survived objects column, which means that the ad windows are still alive. After we closed the windows, the objects should have been removed from the heap. Nevertheless, something prevented them from being collected.
It’s time to start our investigation and find out why our windows were not removed!
Step 4. Analyze Snapshot
As mentioned in Tutorial 1 - Getting Started with dotMemory*, you should think of your work in dotMemory as of crime investigation. You start your investigation by analyzing a huge list of suspects (objects) and continuously narrow the list until you find the one that causes the issue. Your chain of reasoning is shown in the so-called Analysis Path on the left side of the dotMemory window.
Let’s try this approach in action.
- Open the survived
GameOfLife.AdWindowobject set consisting of two objects. To do this, click the number 2 in the Survived objects column next to the
As the object exists in both snapshots, dotMemory will prompt you to specify in which snapshot the object should be shown. Of course, we’re interested in the last snapshot where the windows should have been collected.
- Select Open “Survived Objects” in the newer snapshot and click OK.
This will show the object set “All objects of the AdWindow class that exist both in snapshot #1 and #2” in the Type List view. According to the view, the object set contains 2 instances with the shallow size of 952 B. These instances exclusively retain other objects with the total size of 10,676 B.
We’re interested not in the
AdWindowobjects themselves, but in those that retain our ad windows in memory. To figure this out, we should look at the selected object set using the Group by Dominators view. This will show us dominators---the objects that exclusively retain our ad windows in memory.
- To view the list of dominators for the object set, click Group by Dominators in the list of views.
As you can see, ad windows are retained in memory by event handlers, which, in turn, are referenced by instances of the
DispatcherTimerclass. Let’s continue our investigation and try to find more details about those timer objects.
- Right click the DispatcherTimer object set in the list and select Open this object set.
This will open the
DispatcherTimerobject set* in the Type List view. Now, our goal is to understand how these timers relate to the
AdWindowobjects. In other words, how the timers reference our ad windows. To get this info, we should dive deeper and take a look at the specific instance of the
- Open the Instances view and double click any instance of the
DispatcherTimerclass. It doesn't really matter which one you choose, as they obviously have the same relationship with our ad windows.
By default, the instance is shown using the Outgoing References view. This view is used to get the details on the instance’s fields and references.
As you remember, the ad windows are retained by event handlers, which, in turn, are referenced by the
DispatcherTimerinstances. The Outgoing References view shows how exactly this is happening---the ad window is referenced through the
Tickevent handler. It appears that the
AdWindowinstances are subscribed to the
Tickevent of the timers. Let’s look at this in the code.
- To quickly find the required call in the code, let’s use dotMemory. Simply switch to the Creation Stack Traces view.
Here it is! The latest call in the stack that actually creates the timer is the AdWindow constructor. Let’s find it in the code.
- Switch to Visual Studio with the GameOfLife solution and locate the AdWindow constructor.
ChangeAdsmethod to handle the event.
But why is the ad window kept in memory after we close it? The thing is that we subscribed the window to the timer’s event but forgot to unsubscribe it. Therefore, the fix of this leak is quite simple: we need to add some
Unsubscribe()method which should be called when closing the ad window. In fact, the code already contains such a method, and all you need to do is uncomment the
Unsubscribe();line in the window’s
OnClosedevent. Finally, the code should look like this:
As you can see, our ad window uses the
- Now, to make sure the leak is fixed, let’s build our solution and run the profiling again. Click the Profile item in dotMemory’s menu and repeat Step 2. Get Snapshots and Step 3. Compare Snapshots.
That’s it! The
AdWindowinstances are now in the Dead objects column which means they were successfully collected by the time of getting the second snapshot. The leak is fixed!
Truth be told, this kind of leak does occur quite often. So often, in fact, that dotMemory automatically checks your app for this type of leaks.
Thus, if you open the second snapshot that contains the leak and look at the Inspections area on the Snapshot Overview page, you’ll notice that dotMemory has an Event handlers leak check that already contains our
Step 5. Check for Other Leaks
We've fixed the event handler leak, and the ad windows are now successfully collected by GC. But what about the timers that caused our problems? If everything works fine, the timers should be collected as well and should be absent in the second snapshot. Let’s take a look.
- Open the second snapshot in dotMemory. To do this, click the GameOfLife.exe step (the beginning of your investigation) in Analysis Path and then click the Snapshot #2 link for the second snapshot.
- Open the Type List view for the snapshot by clicking the Largest Size link.
- In the opened Type List view, enter dispatchertimer in the filter field. This will narrow the list down, leaving only objects that contain this pattern in their class names.
As you can see, there are 8
DispatcherTimerobjects in the heap.
- Open the
DispatcherTimerobject set by double clicking it.
This will open the set in the Type List view. Now, we need to ensure that this set doesn’t contain the timers created by the ad windows. As the timers were created in the AdWindow constructor, the easiest way to do this is to look at the set using the Group by Creation Stack Trace view.
- Click the Back Traces link in the list of views (under Group by Creation Stack Trace).
This will show us calls starting from the one that directly created the object, and descending to the first call in the stack.
Unfortunately, the AdWindow.ctor(Window owner) call is still here, meaning that the timers created by this call were not collected. They exist in the snapshot regardless of the fact that the ad windows were closed and removed from memory. This looks like one more memory leak that we should analyze.
- Double click the AdWindow.ctor(Window owner) call.
dotMemory will show us the object set (consisting of two timers) in the Type List view.
To figure out what retains the timers in memory, let’s look at the Group by Dominators view.
- Click Group by Dominators in the list of views.
The list of dominators contains just one row, Not exclusively retained objects, which means that each timer is retained in memory by more than one object.
In such cases, the best solution is to look at main retention paths of such 'not exclusively retained' object. For this purpose, dotMemory has a view called Group by Similar Retention.
- Click Group by Similar Retention in the list of views.
The Group by Similar Retention view groups objects in a set by similarity of their retention paths. In addition, this view shows the two most dissimilar retention paths for each group. Typically, this is enough to understand what prevents your object from being collected.
- Click any timer in the list.
As you can see, our timers have slightly different retention paths. In fact, they differ only in one additional
PriorityItemobject; therefore, in our example there's no big difference which of the timer instances to analyze.
The first retention path of our timers leads us to the
DispatcherTimerlist, which is global and stores all timers in the app. The second way shows that the timer is also retained by the
DispatcherOperationCallbackobject. This object is a delegate that is created when you run the timer. This means that the timer is still running. One peculiar thing of the
DispatcherTimerclass is that the instance is removed from the global timer list only after the timer is stopped. Therefore, to fix the leak, we must stop the timer before the ad window is closed. Let’s do this in the code!
- Open the AdWindow.cs file which contains the implementation of the
AdWindowclass. Actually, the fix will be quite simple. All we need to do is add the
adTimer.Stop();line to the
Unsubscribe()method. After the fix, the method should look like this:
- Rebuild the solution.
- Click the Profile item in the dotMemory’s menu and repeat Step 2. Get Snapshots and Step 3. Compare Snapshots.
- Open the second snapshot in the Type List view.
As you can see, there are only 6
DispatcherTimerobjects instead of 8 in the snapshot where we determined the leak. To ensure that GC collected the timers used by the ad windows, let’s look at these timers from the Group by Creation Stack Trace view.
- Double click the DispatcherTimer objects and then click the Back Traces link in the list of views.
Great! There is no AdWindow constructor in the list, which means that the leak has been successfully fixed.
Of course, this type of leak doesn’t seem critical, especially for our app. If we didn’t use dotMemory, we may have never even noticed the issue. Nevertheless, in other apps (for example, server-side ones working 24/7) this leak could manifest itself after some time by causing an OutOfMemory exception.