Its been a while since I posted something in this blog.
I've been dealing with a memory leak issue for the past week so I thought about sharing the experience I gained with you.
First of all, when it comes to memory management in .NET some people think that it shouldn't be trusted so GC.Collect is triggered everywhere they think it will help lowering the memory working set of their application. It's just plain wrong. I had very rare occasions where I needed to explicitly collect objects from the GC.
A good way of diagnosing memory problems is to use the tools available to you. DebugDiag, VMMap and WinDbg should suffice. DebugDiag and VMMap are quite easy to use while WinDbg needs some training before hand.
Things might not be what they seem to be :) In my case I was getting working sets that were about 1,4-1,7GB with .NET Heaps consuming only around 250-300MB. This can lead you into thinking it is an unmanaged leak because you have a lower .net heap memory usage than the process (unmanaged) heap. While it could be the sign of an unmanaged memory leak it can be in fact managed objects being pinned in the GC and keeping references to unmanaged data allocated by them. Example: Bitmaps.
If you take a look at how Bitmaps are structured (you can run !do <bitmapaddress>) you will see a property called nativeImage. This property is nothing more nothing less than a IntPtr that points to an address in the "unmanaged" heap. This is where it gets interesting. The unmanaged heap is keeping all the bitmap data allocated with Gdi+ (used under the hood by System.Drawing).
You might think that by looking at the output of !eeheap -gc you can see how much memory your .NET application is using but this will in fact tell you the total size in the .NET Heaps that your .NET objects consume. Not all your objects' values.
So in the Bitmap case, you will get very low instance size while they can be in fact keeping a great amount of memory in the unmanaged heap.
In case you are experiencing OutOfMemory exceptions or see that your app process is consuming a lot of virtual memory (VAC or <unclassified> - see !address -summary) and the #bytes in all heaps counter is a lot lower than that, this might help you:
- Run DebugDiag and run a Memory Analysis test.
- Exclude the warnings about clr.dll. In my case I was getting warning about WindowsCodecs.dll doing a lot of zcalloc (WindowsCodecs!zcalloc). I actually tried to get the stack of the Heap Allocs that were leading to this.
- What objects are being pinned?
- Use WinDbg to find out: !gchandles. If you see a big number of Bitmaps or System.Drawing.Internal.GPStream you are in big trouble :)
- Why are those objects pinned?
- Use WinDbg to find out: !gcroot <objectaddress> and try to understand the dependency between objects in the output. In my case I had a tooltip keeping a reference to all my buttons (in a data navigator used widely in the application).
- Make sure you Dispose the objects that are keeping your objects pinned.