Search code examples
c#garbage-collectionfinalizer

Object.Finalize() override and GC.Collect()


I cannot seem to understand the behavior of GC.Collect() under the presence of a class overriding Object.Finalize(). This is my base code:

namespace test
{
 class Foo
 {
  ~Foo() { Console.WriteLine("Inside Foo.Finalize()"); }
 }

 static class Program
 {

  static void Main()
  {
   {
    Foo bar = new Foo();
   }

   GC.Collect();
   GC.WaitForPendingFinalizers();

   Console.ReadLine();
  }
 }

}

Contrary to what I was expecting, I only get the console output at program termination and not after GC.WaitForPendingFinalizers()


Solution

  • Neither the compiler nor the runtime are required to guarantee that locals that are out of scope actually have the lifetimes of their contents truncated. It is perfectly legal for the compiler or the runtime to treat this as though the braces were not there, for the purposes of computing lifetime. If you require brace-based cleanup, then implement IDisposable and use the "using" block.

    UPDATE:

    Regarding your question "why is this different in optimized vs unoptimized builds", well, look at the difference in codegen.

    UNOPTIMIZED:

    .method private hidebysig static void  Main() cil managed
    {
      .entrypoint
      // Code size       28 (0x1c)
      .maxstack  1
      .locals init (class test.Foo V_0)
      IL_0000:  nop
      IL_0001:  nop
      IL_0002:  newobj     instance void test.Foo::.ctor()
      IL_0007:  stloc.0
      IL_0008:  nop
      IL_0009:  call       void [mscorlib]System.GC::Collect()
      IL_000e:  nop
      IL_000f:  call       void [mscorlib]System.GC::WaitForPendingFinalizers()
      IL_0014:  nop
      IL_0015:  call       string [mscorlib]System.Console::ReadLine()
      IL_001a:  pop
      IL_001b:  ret
    } // end of method Program::Main
    

    OPTIMIZED:

    .method private hidebysig static void  Main() cil managed
    {
      .entrypoint
      // Code size       23 (0x17)
      .maxstack  8
      IL_0000:  newobj     instance void test.Foo::.ctor()
      IL_0005:  pop
      IL_0006:  call       void [mscorlib]System.GC::Collect()
      IL_000b:  call       void [mscorlib]System.GC::WaitForPendingFinalizers()
      IL_0010:  call       string [mscorlib]System.Console::ReadLine()
      IL_0015:  pop
      IL_0016:  ret
    } // end of method Program::Main
    

    Obviously a massive difference. Clearly in the unoptimized build, the reference is stored in local slot zero and never removed until the method ends. Therefore the GC cannot reclaim the memory until the method ends. In the optimized build, the reference is stored on the stack, immediately popped off the stack, and the GC is free to reclaim it since there is no valid reference left on the stack.