4
votes

If I have a loop such as this:

public class Foo {
     public Foo Foo;

     public Foo() {
     }
}

class Program {
    public static void Main(string[] args) {
         var foo = new Foo();
         long i = 0;
         while(i < Int64.MaxValue) {
             foo.Foo = new Foo();
             foo = foo.Foo;
             if(i % 10000 == 0)
                 GC.Collect();
             i++;
         }
         GC.Collect();
    }
}

The garbage collector will not clean up the parent objects until the loop is exited. Why is that? I don't see any way to reference them from the code once foo is reassigned, so shouldn't they be cleaned up?

I was looking at the memory usage of the process in Task Manager after passing some breakpoints I had set to determine this was happening. It keeps going up inside the loop (up to multi-GB if I make it infinite), but drops immediately when the loop quits and the second GC.Collect() is called.

1
What "parent objects"? - Stephen C
"The garbage collector will not clean up the parent objects until the loop is exited" : can you describe in your question how you are making those observations ? - quantdev
@quantdev I edited the question. - joelises
I realise this is not your real code, but why not just do foo = new Foo() instead of assigning to foo.Foo? By the way, Public Foo Foo; doesn't compile. - Blorgbeard
What is the name of the column your are looking at in the Task manager ? - quantdev

1 Answers

6
votes

Here's a slightly modified program that demonstrates the behavior more clearly:

class Foo
{
    public int Value;
    public Foo Next;

    public Foo(int value) { this.Value = value; Console.WriteLine("Created " + this.Value); }
    ~Foo() { Console.WriteLine("Finalized " + this.Value); }
}

class Program
{
    public static void Main(string[] args)
    {
        var foo = new Foo(0);
        for (int value = 1; value < 50; ++value)
        {
            foo.Next = new Foo(value);
            foo = foo.Next;
            if (value % 10 == 0)
            {
                Console.WriteLine("Collecting...");
                GC.Collect();
                Thread.Sleep(10);
            }
        }
        Console.WriteLine("Exiting");
    }
}

On .NET 4.5, when I build in Debug mode AND target Any CPU or x86, I reproduce the behavior you're seeing: the instances aren't finalized until after "Exiting" is printed. But when I build in Release mode OR target x64 (even when building in Debug mode), the instances are finalized as soon as they're unreachable:

Created 0
Created 1
Created 2
Created 3
Created 4
Created 5
Created 6
Created 7
Created 8
Created 9
Created 10
Collecting...
Finalized 9
Finalized 0
Finalized 8
Finalized 7
Finalized 6
Finalized 5
Finalized 4
Finalized 3
Finalized 2
Finalized 1
Created 11
Created 12
Created 13
...

Why does this happen? I suppose only a CLR expert can tell us for sure, but here's my guess: the behavior depends on specific details of the machine code that the JIT compiler and optimizer happen to generate, details which vary based on the target instruction set and whether you're running in debug mode. (Furthermore, these details may well change in future versions of the runtime.) In particular, in the x86/Debug case, I think the first Foo(0) instance gets stashed in a register or stack variable that never gets overwritten in the rest of the method; this initial instance keeps the entire chain alive. In the x86/Release and x64 cases, I think that due to JIT optimizations, the same register or stack variable is reused for every instance, thus releasing the initial instance.