views:

464

answers:

5

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()

+3  A: 

The garbage collector makes absolutely no guarantee as to when data will be collected. This is one of the reasons why you need to use the using statement for disposing disposable objects.

GC.WaitForPendingFinalizers() only waits for finalizers which have been collected - if an object wasn't yet collected it does nothing.

Most likely the compiler is keeping a pointer to bar around even though you don't have access to the name anymore.

I would try putting the call to new Foo() in a separate function - that might help, although again - no guarantee.

Aaron
+11  A: 

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.

Eric Lippert
Ok. that explains a lot. thanks.
Krugar
Excellent update to your post, Eric. Many thanks!
Krugar
+2  A: 

bar is still in scope when you call GC.Collect() and GC.WaitForPendingFinalizers()

Foo also doesn't implement IDisposable().

My guess is that the GC isn't ready to free up the memory in use by your Foo object yet, and you can't explicitly call Dispose(). Therefore, it's getting disposed when the application completes its execution.

Justin Niessner
After I implement dispose, Foo is indeed properly garbage collected when I expect it too. Thanks.
Krugar
A: 

I don't think scoping works the same way as in C++. I think the variables are actually valid until function exit, eg:

class Program
{
    class Foo
    {
        ~Foo() { Console.WriteLine("Test"); }
    }


    static void Test()
    {
        Foo foo = new Foo();
    }

    static void Main()
    {
        Test();

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

        Console.ReadLine();
    }
}

If you think about the IL, then there is no such thing as a brace in IL and local variables always have at least function scope.

avid
Not sure what your code tries to prove. In any case, you can enforce scope under C# by using braces. And being local variables, these can be garbage collected while still under the same function. Why it wasn't happening is explained on the other posts.
Krugar
A: 

Here is another great article about GC may occur at unexpected point of code execution:

Lifetime, GC.KeepAlive, handle recycling - by cbrumme http://blogs.msdn.com/b/cbrumme/archive/2003/04/19/51365.aspx?wa=wsignin1.0

My question is how can I reproduce forced GC at the point mentioned in the article? I tried to put GC.Collect() at beginning of OperateOnHandle(), and defined destructor for class C but seems not working. Destructor is invoked always at end of program.

Sheen