Strategies for memory profiling
page 1 of 1
Published: 01 May 2010
Company Logo

This is a whitepaper provided by one of our AspAlliance sponsors. These whitepapers are intended to provide you with information on products and services that we consider useful and of value to developers.

Abstract
In this whitepaper, Red Gate discusses the importance of handling two common issues in memory management: memory leaks and excessive memory usage. Red Gate demonstrates how their ANTS Memory Profiler can identify issues with memory management and provide a detailed view of a program's memory usage. This whitepaper doubles as a brief tutorial for using the ANTS Memory Profiler by providing an example of a program that is experiencing memory management issues.
by Red Gate
Feedback
Average Rating: This article has not yet been rated.
Views (Total / Last 10 Days): 10093/ 14

Strategies for Memory Profiling

OK, so your application has a memory problem. The first thing to remember is: don't panic, because it's definitely fixable. Having spent the last few years visiting customer sites to find and fix .NET memory and performance issues, I quickly discovered that all you need to be effective is a great tool and a clear strategy.

Two of the biggest memory problems I typically come across are memory leaks and excessive memory usage. Unsurprisingly, memory leaks get most attention, but an application that uses excessive memory can cause big problems too.

In this article, I'm going to talk about both issues, and how you can use ™ 5 from Red Gate to resolve them. I'll look at:

·         Why memory leaks matter

·         Why excessive memory usage matters

·         How to find a memory leak

·         How to check an application's memory usage

 

 

 

Why should I care about memory leaks?

Memory leaks are feared by developers because the assumption is that they can't happen in .NET; and then, when they do, that they are impossible to fix. Neither of these is true; you just need to know how to approach fixing memory leaks.

The profile of a leaking application is typically:

·         Memory usage slowly increases over time

·         Performance degrades

·         Application will freeze/crash requiring a restart

·         After restart, the application runs without problems again, and the cycle repeats.

 

Memory leaks occur when a reference is retained to an object of which instances are created frequently. Finding a memory leak is simply a matter of finding the object that is being retained, and then determining which reference in your code is causing it to be retained.

The basic strategy I describe in this article will help you find memory leaks. You can also use the technique on a more regular basis as a validation check, to ensure completed units of code are leak-free. Being able to identify when code isn't leaking is even more useful – because, most of the time, that's the case.

 


Why should I care about excessive memory usage and fragmentation?

Reducing the overall memory footprint can help an application co-exist with other applications on the desktop or server.

It's always worth checking your application is using memory efficiently and caching large objects only when necessary. I once had to analyze an ASP.NET application which kept failing, putting the developers under a lot of pressure. Memory analysis identified the problem as a simple session-state caching issue. Whenever a user logged on to the system, the application retrieved a large data set from the database, containing security information for the user, which was placed in session state. The session-state timeout was also raised to 90 minutes. As this was an application hit by thousands of users an hour, the server couldn't cope. The worst thing about this memory leak was that the data set wasn't actually used again after it was cached.

The reason I've described this example is not because I think storing state is wrong; it's because I think you should always ensure that the state you are maintaining is valid and necessary, especially if it is significantly large. Checking your application's memory footprint will highlight issues you don't realize are there.

Typical symptoms of an application with an excessive memory footprint include:

·         Application is slow to load and runs slowly

·         When the application is loaded other applications run slowly.

 

In this article, I show how to use ANTS Memory Profiler to find the unnecessary memory bloat in an application. We will also explore how allocating objects of > 85 KB can cause large object heap fragmentation and lead to an out-of-memory exception, when it seems there is plenty of memory available on the heap.

 

 

 

Finding a memory leak

A well-behaved application transaction (e.g. UI sequence or server process call) should perform its actions and then clean up after itself. The net effect on the running application should ideally be zero after completion.

Memory-leak detection is all about recording the application "at rest" and then again after a test transaction is executed. Objects left behind should be investigated further and, if new objects of the same type are created after every iteration, it should raise suspicions that there's a memory leak.

If you suspect your application has a leak, you need to identify the most likely application transactions (use-cases, processes) that may be responsible. Next, run each transaction a number of times, taking a memory snapshot with ANTS Memory Profiler at the end of each test. By comparing snapshots and cleverly using filters to eliminate the "application noise," you'll be able to focus in on the memory leak.

When you are trying to find a memory leak it's crucial that you have a clear strategy and stick to it; otherwise it's easy to be distracted by irrelevant data and follow the wrong path.

To find a memory leak you will need:

·         Documented test transaction, including any necessary clean-up steps.
For example, a sequence of web service or network calls or a sequence of user-interface actions ("Enter x ...", "Click y ...").

·         ANTS Memory Profiler 5.


Basic strategy for finding a memory leak

Here are the steps we're going to take:

1.    Start your application and take memory snapshots.

2.    Take a baseline snapshot.

3.    Run the test process.

4.    Filter out the noise.

5.    Analyze the results.

6.    Investigate suspicious classes.

7.    Investigate instances of a suspicious class.

8.    Identify the cause of the memory leak.

To illustrate the basic strategy, I've written an application containing a memory leak I've seen many times at customer sites. For "background noise," I've added a timer that runs every 5 seconds, querying a remote database. This should leave lots of "red herring" objects on the heap whenever I take a memory snapshot, and will closely mimic what happens in a real-world application. The key skill to learn is how to decide what's significant ... and what's irrelevant.

 

1. Start your application and take memory snapshots

·         Open , either from within Visual Studio or from the Red Gate Programs folder, and start profiling your application.

 

Figure 1: ANTS Memory Profiler start-up window

 

2. Take a baseline snapshot

·         Click the Take Memory Snapshot button to take your first snapshot.

ANTS Memory Profiler forces a garbage collection, then takes a snapshot of every object still left in memory, and displays the results.

This first or "baseline" snapshot is crucial because it captures the memory state before we have executed the test transaction. Comparing back to this baseline will help identify possible memory leaks in the subsequent test transactions. Figure 2 shows the baseline snapshot in my test.

Figure 2: Snapshot summary window

 


3. Run the test process

We're now ready to begin the test:

·         Run your test transaction, including any clean-up steps, and then take a memory snapshot.

·         Repeat this at least twice more.

At the end of the test process you will have at least four snapshots (including our baseline). The greater the number of test transactions the better because, if there is a memory leak, it will continue to get bigger each time you run the test transaction – and that will make it easier to see the issue.

Figure 3: Comparing snapshots

The results summary in Figure 3 shows our second snapshot (snapshot 2) with a comparison against the baseline snapshot (snapshot 1) on the right.

 

4. Filter out the noise

Before we start to look at the results in detail, it's a good idea to filter out some of the irrelevant noise. Filtering out the noise reduces the chances of finding false positives, and cuts down on the amount of work we need to do. ANTS Memory Profiler has some great filters to help.

From the Standard object filters:

·         Choose Comparing snapshotsOnly new objects.

We only need to see the new objects left behind since the previous snapshot, as they are possible leak candidates.

 

From the Advanced object filters:

·         In the Kept in memory only by GC roots of type filter, clear the Finalizer queue check box.

This eliminates objects that are waiting for finalization. These objects are waiting for the .NET finalization thread to run, have no other references, and will disappear from the trace anyway. Filtering these objects out removes unnecessary noise from our trace.

·         From the Objects which are/are not GC roots, select Objects which are not GC roots.

A memory leak is extremely unlikely to be a GC root.

 

5. Analyze the results

During the test process we captured a number of snapshots, including the baseline. We now need to analyze each snapshot and look at the classes that survived garbage collection.

To do this, let's look at the list of classes that use the most memory:

·         Click the Class List button.

The class list displays all of the classes with new object instances allocated since the previous snapshot. It can be useful to sort the list by descending class size; click the top of the Live Size column to do this.

 

Figure 4: The class list, showing classes left in memory


The class list is where the real work is done. We could just work through each class in turn and look at each object instance and its references. That would take some time, so we're going to improve our chances of hitting the right class straight away, by comparing each snapshot to the previous one and looking for same classes that always appear.

We performed the same actions in each repeat of the test process, so any memory leak should be consistent, and a leaking class should appear in each snapshot. If the same class does appear in each of the snapshots, it goes to the top of my priority list for investigation.

 

To compare snapshots, use the snapshot toolbar.

 

Figure 5: The snapshot toolbar

 

In Figure 6, snapshot 3 and snapshot 2 are compared. Order, Tick, and string are consistently appearing in the snapshots, so they go straight to the top of my priority list for further investigation.

 

Figure 6: Viewing the class list and comparing the snapshots

Once you have your suspicions about a class, you can often confirm its leak profile by watching the memory leak get worse over the snapshots in the class list. Compare each snapshot against the baseline, starting with snapshot 2; your suspicions will be confirmed if the Live Size for your leaking class increases each time.

6. Investigate suspicious classes

Every class in the class list is there because it has instances that survived garbage collection. We need to find out whether we can trace any of those instances back to references in our code. If we can, then we can either fix the problem, or confirm that it is intended behavior and won't cause a problem.

To do this, we need to look at each class in detail. I recommend starting with your list of suspicious classes first, and only then work through the class list, starting at the top (ordered by descending Live Size) until you find a memory leak.

To do this:

·         Select the class in the class list, then click the Class Reference Explorer button.

 

Figure 7: The class reference explorer

 

Figure 7 shows the class reference explorer for the Order class, a graph of all of the classes that have object instances with references to Order classes. We can use this graph to trace backwards through reference paths to find which of our own classes, if any, reference the Order class:

·         Click on a class to expand its referencing class (leftwards).

 

In Figure 7, notice MemoryAnalysis.Tick contains a percentage value (100%). This indicates that instances of Tick classes are responsible for 100% of the references to instances of Order classes.

As we are trying to trace back to find if it is one of our own classes, all we need to do is keep following the class path backwards, following the path with the highest percentage reference contribution, until we come to a class we recognize.

If you get to a GC root when you are expanding references to a class leftwards, move on to the class with the next highest percentage value, and expand references to that class leftwards.

 

7. Investigate instances of a suspicious class

When we reach a class we recognize, we can investigate instances of the class:

·         Right-click on the source class, and select Show Instance List for <class> on this path.

A list of the actual object instances of Order is displayed.

 

Figure 8: The object instance list

The object instance list in Figure 8 lists all of the object instances of our potentially leaking class referenced by our own code.

From here, comparing against the baseline snapshot and walking upwards through the snapshots (from 2 to 4) should confirm whether there's a memory leak. If there is one, we'd expect to see the leaked objects building up in the list after each snapshot.

 


8. Identify the cause of the memory leak

Finally, we need to identify what in the source code is holding the reference that creates the memory leak, so that we can fix it:

·         Select one of the objects in the list, and click the Object Retention Graph button.

 

Figure 9 shows the object retention graph for Order, and the object references keeping the selected object in memory. It also provides the variable names!

 

Figure 9: The object retention graph

 

By working up the object retention graph from MemoryAnalysis.Order we can see that MemoryAnalysis.CurrencyManager is ultimately holding a reference to our selected object. The reference is caused by an instance variable m_Currency of type MemoryAnalysis.CurrencyManager from an event source OnPriceUpdate.

This is one of the most common .NET memory leaks I have come across, and it's in a technique many .NET developers use.


My application is a simple currency trader. It creates a Currency Order and adds it to a Deal List. It uses a CurrencyManager class that constantly updates the currency price via an OnPriceUpdate event. Each Order subscribes to the OnPriceUpdate event using its Order.OnTick method.

Order newOrder=new Order("EURUSD", DealType.Buy, Price, PriceTolerance, TakeProfit, StopLoss);

newOrder.OnPlaced+=OrderPlaced;

m_Currency.OnPriceUpdate+=newOrder.OnTick;

m_PendingDeals.Add(newOrder);

 

When the price is right an Order completes; it calls the OnPlaced event which is handled by the OrderPlaced method.

void OrderPlaced(Order placedOrder)

{

m_PendingDeals.Remove(placedOrder);

m_ActiveDeals.Add(placedOrder);

}

The OrderPlaced method is missing one line which is needed to avoid a memory leak.

m_Currency.OnPriceUpdate -= placedOrder.OnTick;

 

Memory leak found!

In many cases, there are alternative ways to find a leak using . For example, in this case we could have found the memory leak using the Kept in memory only by event handlers filter.

Finding a leak when you don't know what transactions are causing it

If you're not sure which test transactions may be responsible, then it's a good idea to monitor the executing application running within ANTS Memory Profiler. Watch the timeline and, when memory usage starts to increase, take a number of snapshots. The number and interval between depends on the application's behavior. It's likely the application will behave the same way repeatedly, so get used to the memory usage spike, and take a set number of snapshots over this time frame. Then use the same techniques outlined above to find out if there is a memory leak.

Making sure the memory leak is fixed

This is the easy bit, because all you need to do is repeat the process and make sure the class behavior has disappeared. Run exactly the same test process and snapshots, and compare the traces with the pre-fix traces.

Recognizing when you don't have a memory leak

As I mentioned earlier, a memory leak is usually consistent in that the same sequence of actions causes the memory leak. So, when you run a test, as long as you repeat the test exactly, you can validate that it doesn't have a memory leak by comparing the class list between snapshots, as described in the Basic strategy for finding a memory leak section. Leaking classes should consistently appear in each snapshot. If no classes appear consistently, it's unlikely you have a memory leak. The classes that do appear should be investigated further. If you find no references back to code using the class reference explorer and the object retention graph, then you are not causing a memory leak.

 

Checking an application's memory usage

Investigating an application's memory footprint

To determine an application's or test transaction's memory footprint, you need to find out which class instances survive from beginning to end.

1.    Take a baseline snapshot.

2.    Wait for the memory trace graph in the timeline to stabilize.

3.    Take another snapshot.

4.    Choose standard object filter – Comparing snapshots Only surviving objects.

5.    Click the Class List button to view the class list data.

 

Figure 10: Memory footprint snapshot

 

Figure 10 gives a summary of the large allocated classes after the application has loaded.

 


To investigate this further, click the Class List button.

 

Figure 11: Class list for memory footprint

 

Investigate the classes with the largest Live Size:

·         Select the class with the largest footprint (in this case byte[]).

·         Click the Class Reference Explorer button.

 

Figure 12: Class reference explorer for the memory footprint analysis

 

The class reference explorer allows us to trace backwards through the class references to find the classes that have object instances that are responsible for the majority of the references. If we can ultimately trace these references back to our own classes, then we then can decide whether to optimize the code to remove the references and thereby reduce the footprint.

Just as with memory leak detection:

·         Follow the class path with the highest % contribution until you reach a class you recognize.

·         Right-click on the source class and select Show Instance List for <class name> on This Path.

A list of object instances for this class is displayed.

 

Figure 13: Object instances for memory footprint

 

Each one of the objects in Figure 13 is adding to the footprint; to investigate their origin, select the top one and view the object retention graph.

 

Figure 14: Object retention graph for memory footprint

 

The object retention graph in Figure 14 illustrates that the byte[] array is being held in memory by a Generic List within my own MemoryAnalysis.Form1 class with a variable name of m_LOH. I have all the information I need from here to make a decision about this collection and whether its contribution to the application footprint is justified.

 


Investigating large object heap fragmentation

If your application allocates objects larger than 85K, they will be allocated onto the large object heap (LOH). Unlike the small object heap (for objects <=85K) the LOH doesn't remove the gaps left between objects caused when an object is garbage collected. This can lead to memory fragmentation and to an out of memory exception when there is still space on the heap (in the gaps!).

The snapshot summary gives an overview of the memory state of the large object heap. You can use this to identify symptoms of fragmentation. Three statistics are provided:

·         Free space on all.NET heaps (total memory reserved)

·         Largest free block (largest reserved memory block)

·         Max. size of new object.

 

If the Free space on all .NET heaps statistic is large and the Largest free block is small, this can indicate a fragmentation problem. It basically means there are lots of small chunks of fragmented memory available. It doesn't necessarily mean there will be an out of memory exception, though. If the Max. size of new object is still large, then the size of the heap can still be expanded by reserving more memory, increasing the application footprint.

If Max. size of new object is small as well, though, an out of memory exception is on its way! This is the classic LOH fragmentation condition.

 

For more information, Andrew Hunter has written an excellent article on this subject: The dangers of the large object heap.

 

Summary

Before you check a completed unit of work back into the source control system, I advise you to:

1.    Run each of your test transactions against the memory leak basic strategy.

2.    Look for anything that anything that is consistently "there" and investigate it further.

3.    If there are no issues, which most of the time there won't be, then you're done!

4.    If there are issues, fix them!

Don't leave it until it goes into system or load testing or, even worse, production, because you can avoid a lot of work later by finding and fixing the issues now.

 

About the author:

Chris Farrell is a development consultant, technical trainer and developer with seven years of .NET experience. He is an expert in .NET memory management and application performance optimization.



User Comments

No comments posted yet.




Community Advice: ASP | SQL | XML | Regular Expressions | Windows


©Copyright 1998-2024 ASPAlliance.com  |  Page Processed at 2024-04-25 3:53:42 AM  AspAlliance Recent Articles RSS Feed
About ASPAlliance | Newsgroups | Advertise | Authors | Email Lists | Feedback | Link To Us | Privacy | Search