Chilkat takes every report of a memory leak seriously, but quite often what seems like a memory leak is in fact not one at all. It’s simply a matter of not understanding how the .NET garbage collection works, or what to expect from the garbage collection. This blog post describes normal behavior which is often mistaken for a memory leak. Let’s begin with the simplest C# program. We create a brand new C# project in Visual Studio 2005. It’s an empty Form. We do nothing other than compile the Release version and run it. It displays an empty Form. We start the Windows Task Manager and see that it occupies about 16MB of memory. OK — so that’s the baseline — the bare minimum C# program consumes 16MB.
Note: The computer on which these tests are performed has 2GB of memory.
OK, so now the 2nd test, which will use the Chilkat MHT class. We add a button to the form and insert some code:
private void button1_Click(object sender, EventArgs e)
{
Chilkat.Mht mht = new Chilkat.Mht();
mht.UnlockComponent("TEST");
}
If you run it and click the button, you’ll find that it consumes about 20MB. OK, so adding a reference to the Chilkat .NET component and instantiating an Mht object consumed an extra few MB.
Now… what happens if the following code is run?
private void button1_Click(object sender, EventArgs e)
{
int i;
for (i = 0; i < 600000; i++)
{
Chilkat.Mht mht2 = new Chilkat.Mht();
}
}
Oh my!!! Chilkat must be leaking memory! The memory consumption shoots up to over 500MB and then slowly climbs to somewhere between 700 and 800 MB before memory consumption begins fluctuating up and down, and finally it settles down to a mere 124MB when the loop is finished running. What is going on?
The answer is that the .NET garbage collection is allowing that much memory to be used. For whatever reason (probably based on the amount of memory available), the .NET runtime decided that it’s OK to let memory consumption grow to a point, and then it does partial garbage collection to keep memory from growing beyond a certain point. Also, the code took 104 seconds to run (i.e. 1 minute and 44 seconds).
What happens if we force a full garbage collection every 100 cycles? Here’s the code:
private void button1_Click(object sender, EventArgs e)
{
int i;
for (i = 0; i < 600000; i++)
{
Chilkat.Mht mht2 = new Chilkat.Mht();
if ((i % 100) == 0)
{
GC.Collect();
GC.WaitForPendingFinalizers();
}
}
}
The memory consumption stays steady at 21MB. It never grows beyond that, and certainly doesn’t spike up to 500MB. Also, this code only took 27 seconds to run. Forcing the garbage collection reduced the time to run from 104 seconds to 27 seconds.
In conclusion — it may be disturbing that the .NET runtime allows memory usage to grow so much. If you suspect a memory leak, especially with a process that runs over a long period of time (as a Windows Service for example), make sure to add some forced garbage collections. Don’t forget to call both GC.Collect as well as GC.WaitForPendingFinalizers. You may find that the memory consumed is simply due to a lazy .NET garbage collection algorithm…