I've encountered an issue with finalizable objects that doesn't get collected by GC
if Dispose()
wasn't called explicitly.
I know that I should call Dispose()
explicitly if an object implements IDisposable
, but I always thought that it is safe to rely upon framework and when an object becomes unreferenced it can be collected.
But after some experiments with windbg/sos/sosex I've found that if GC.SuppressFinalize() wasn't called for finalizable object it doesn't get collected, even if it becomes unrooted. So, if you extensively use finalizable objects(DbConnection, FileStream, etc) and not disposing them explicitly you can encounter too high memory consumption or even OutOfMemoryException
.
Here is a sample application:
public class MemoryTest
{
private HundredMegabyte hundred;
public void Run()
{
Console.WriteLine("ready to attach");
for (var i = 0; i < 100; i++)
{
Console.WriteLine("iteration #{0}", i + 1);
hundred = new HundredMegabyte();
Console.WriteLine("{0} object was initialized", hundred);
Console.ReadKey();
//hundred.Dispose();
hundred = null;
}
}
static void Main()
{
var test = new MemoryTest();
test.Run();
}
}
public class HundredMegabyte : IDisposable
{
private readonly Megabyte[] megabytes = new Megabyte[100];
public HundredMegabyte()
{
for (var i = 0; i < megabytes.Length; i++)
{
megabytes[i] = new Megabyte();
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
~HundredMegabyte()
{
Dispose(false);
}
private void Dispose(bool disposing)
{
}
public override string ToString()
{
return String.Format("{0}MB", megabytes.Length);
}
}
public class Megabyte
{
private readonly Kilobyte[] kilobytes = new Kilobyte[1024];
public Megabyte()
{
for (var i = 0; i < kilobytes.Length; i++)
{
kilobytes[i] = new Kilobyte();
}
}
}
public class Kilobyte
{
private byte[] bytes = new byte[1024];
}
Even after 10 iterations you can find that memory consumption is too high(from 700MB to 1GB) and gets even higher with more iterations. After attaching to process with WinDBG you can find that all large objects are unrooted, but not collected.
Situation changes if you call SuppressFinalize()
explicitly: memory consumption is stable around 300-400MB even under high pressure and WinDBG shows that there are no unrooted objects, memory is free.
So the question is: Is it a bug in framework? Is there any logical explanation?
More details:
After each iteration, windbg shows that:
- finalization queue is empty
- freachable queue is empty
- generation 2 contains objects(Hundred) from previous iterations
- objects from previous iterations are unrooted