Determinism in Dispose and Finalize Execution Timing

...Time... by ĐāżŦ {mostly absent}, on FlickrI get a surprising number of questions about when objects are disposed or collected. Usually people will have some limited logic to cleanup their objects (releasing file handles, removing references to large objects, etc.), and they don’t know when this logic will execute. Here is a little bit of insight into when objects are disposed and finalized. (If you are looking for more information about Disposable and Finalizable patterns, check this post)

When Does Dispose Execute?

The Dispose method on an object is only executed when it is called by the application. What I mean by that, is the CLR does not automatically call Dispose on your objects. If you don’t call dispose, it can result in a memory leak that does not get cleaned up until the process exits.

This doesn’t mean you necessarily have to call Dispose() explicitly. In C#, there are a number of times that Dispose() is called implicitly:

using()

This is probably the most straightforward. As a matter of fact, this is almost like calling Dispose explicitly, and it will dispose the object, even if an exception is raised from the using block.

using (var file = File.Open(".\\temp.txt", FileMode.Append))
{
    file.Write(new byte[] {0x2a}, 0, 1);
}

If we look at the compiled IL, we can see that it is compiled down into a try-catch-finally, with a call to Dispose() in the finally block.

IL_0000: ldstr ".\\temp.txt"
IL_0005: ldc.i4.6
IL_0006: call class [mscorlib]System.IO.FileStream [mscorlib]System.IO.File::Open(string, valuetype [mscorlib]System.IO.FileMode)
IL_000b: stloc.0
.try
{
  IL_000c: ldloc.0
  IL_000d: ldc.i4.1
  IL_000e: newarr [mscorlib]System.Byte
  IL_0013: stloc.1
  IL_0014: ldloc.1
  IL_0015: ldc.i4.0
  IL_0016: ldc.i4.s 42
  IL_0018: stelem.i1
  IL_0019: ldloc.1
  IL_001a: ldc.i4.0
  IL_001b: ldc.i4.1
  IL_001c: callvirt instance void [mscorlib]System.IO.Stream::Write(uint8[], int32, int32)
  IL_0021: leave.s IL_002d
} // end .try
finally
{
  IL_0023: ldloc.0
  IL_0024: brfalse.s IL_002c
  IL_0026: ldloc.0
  IL_0027: callvirt instance void [mscorlib]System.IDisposable::Dispose()
  IL_002c: endfinally
} // end handler
IL_002d: ret

foreach

Here’s one you may not know. Enumerator is disposable. Foreach gets an enumerator, and then disposes it for you.

var fileStreams = new List()
                        {
                            File.Open(".\\temp1.txt", FileMode.Append),
                            File.Open(".\\temp2.txt", FileMode.Append)
                        };
 
foreach (var file in fileStreams)
{
    file.Write(new byte[] { 0x2a }, 0, 1);
}

And here is the compiled IL. Notice, it does NOT dispose the objects in the collection… only the enumerator. Be warned! If you manually retrieve an enumerator, you need to dispose it!

IL_0000: newobj instance void class [mscorlib]System.Collections.Generic.List`1::.ctor()
IL_0005: stloc.2
IL_0006: ldloc.2
IL_0007: ldstr ".\\temp1.txt"
IL_000c: ldc.i4.6
IL_000d: call class [mscorlib]System.IO.FileStream [mscorlib]System.IO.File::Open(string, valuetype [mscorlib]System.IO.FileMode)
IL_0012: callvirt instance void class [mscorlib]System.Collections.Generic.List`1::Add(!0)
IL_0017: ldloc.2
IL_0018: ldstr ".\\temp2.txt"
IL_001d: ldc.i4.6
IL_001e: call class [mscorlib]System.IO.FileStream [mscorlib]System.IO.File::Open(string, valuetype [mscorlib]System.IO.FileMode)
IL_0023: callvirt instance void class [mscorlib]System.Collections.Generic.List`1::Add(!0)
IL_0028: ldloc.2
IL_0029: stloc.0
IL_002a: ldloc.0
IL_002b: callvirt instance valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator class [mscorlib]System.Collections.Generic.List`1::GetEnumerator()
IL_0030: stloc.3
.try
{
  IL_0031: br.s IL_0053
  // loop start (head: IL_0053)
  IL_0033: ldloca.s CS$5$0000
  IL_0035: call instance !0 valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator::get_Current()
  IL_003a: stloc.1
  IL_003b: ldloc.1
  IL_003c: ldc.i4.1
  IL_003d: newarr [mscorlib]System.Byte
  IL_0042: stloc.s CS$0$0001
  IL_0044: ldloc.s CS$0$0001
  IL_0046: ldc.i4.0
  IL_0047: ldc.i4.s 42
  IL_0049: stelem.i1
  IL_004a: ldloc.s CS$0$0001
  IL_004c: ldc.i4.0
  IL_004d: ldc.i4.1
  IL_004e: callvirt instance void [mscorlib]System.IO.Stream::Write(uint8[], int32, int32)
  IL_0053: ldloca.s CS$5$0000
  IL_0055: call instance bool valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator::MoveNext()
  IL_005a: brtrue.s IL_0033
  // end loop
  IL_005c: leave.s IL_006c
} // end .try
finally
{
  IL_005e: ldloca.s CS$5$0000
  IL_0060: constrained. valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator
  IL_0066: callvirt instance void [mscorlib]System.IDisposable::Dispose()
  IL_006b: endfinally
} // end handler

Others?

If you know of any other interesting facts related to object disposal, leave them in the comments below!

When Does The Finalizer Run?

You will hear and read that finalizer execution is non-deterministic. What they mean is that you cannot predict when the finalizer will run. This is true to some extent, but there are some things that we do know:

  1. Garbage collection runs, and identifies an object as “dead”
  2. This dead object has a finalizer (and GC.SuppressFinalize() has not been called)
  3. The GC places this object into the “finalizer queue”
  4. After GC is completed, the finalizer thread resumes and starts running finalizers from the queue
  5. The finalizer thread is a managed thread, and runs at the HIGHEST thread priority

So this is what we know… an object’s finalizer will run sometime after the GC has identified the object as dead and placed on the finalizer queue. Not exactly precise, but not entirely unpredictable.

Leave a Reply