CLR programs have memory leaks, just like unmanaged ones, but for a different reason. There are lots of opportunities to add your object to a list - the major example being enlisting for events. If the event source lives for the lifetime of the program, then any objects listening for that event will also last for the lifetime of the program. So if you accidentally leave an object enlisted on such an event, you have created a memory leak - much like forgetting to call delete after calling new in C++.
But what is far better about the CLR is that it can pretty much tell you exactly what is causing a leak, although the technique is only documented on a few blog posts here and there. Also, they seem to be written by people who prefer using windbg. But after some fooling around, I've made it work in Visual Studio, which is a lot more convenient.
Run your program in the Visual Studio 2008 debugger, and get it into the state where you think something has leaked. Somehow make it break into the debugger in some CLR code. Then type the magic words into the Immediate Window.
These are the magic words:
.load C:\Windows\Microsoft.NET\Framework\v2.0.50727\sos.dll GC.Collect() !DumpHeap -type <some-type-name> !gcroot <some-address>
The path to sos.dll varies. The way to find out the correct path is to look for mscorwks.dll in the Modules pane. Wherever that is loaded from is the correct path for sos.dll.
(There's supposed to be a command .loadby that would be easier to use, but it doesn't appear to exist in Visual Studio 2008 - this is the part that I didn't see documented anywhere).
The incantation GC.Collect() will force a garbage collection. If your object survives after that, then something is keeping it alive, and you want to know what.
First you need to know the type name of your object, including the namespace qualification. You use that !DumpHeap incantation to find instances of your object. Here's what good news looks like (i.e. no objects found):
!DumpHeap -type MyNamespace.MyCrazyClass Address MT Size total 0 objects Statistics: MT Count TotalSize Class Name Total 0 objects
And here's what bad news looks like:
!DumpHeap -type MyNamespace.MyCrazyClass Address MT Size 02f768b0 06412a64 468 02fcdba0 06412a64 468 total 2 objects Statistics: MT Count TotalSize Class Name 06412a64 2 936 MyNamespace.MyCrazyClass Total 2 objects
So here I have two objects hanging around - why?
Note that the output above starts with a table of all the instances. The first column is the address of each object. You pass one of those addresses to !gcroot and it dumps out a trace of at least one route via which the object is being kept alive.
That is, starting from somewhere on the stack and working through all the objects that hold references to other objects, until it gets to your object.
Each line of the !gcroot output starts with the object address, so if you search for occurrences of your object's address, you may find more than one line that is the end of a route to your object. So it's useful to paste the output into notepad and insert line breaks after each of these lines, so you can see the separate routes.
Then from your object, work your way back to see who's keeping who alive.
The output starts with a warning:
Note: Roots found on stacks may be false positives. Run "!help gcroot" for more info.
So I just fix the issues that I can figure out the cause of, until the leak goes away. I figure this means that the things I didn't fix were false positives (which seems reasonable).