3

Situation

According to this accepted answer, if the "GC 'sees' a cyclic reference of 2 or more objects which are not referenced by any other objects or permanent GC handles, those objects will be collected."

I wanted to know if garbage collection works for a super simple tree structure that doesn't even have content, just tree-nodes with parent- & child-references.

Imagine you create a root-node add a child to it and then a child to the child and so on, so not really a tree but more like a list (each node has at most one child and one parent).

If we then remove the root's child and all references to node's within this child's subtree as I understand the answer above, the garbage collector should clean up the subtree.

Problem Description

If you have a look at the Main-method in the test-code below, when running the exe from the Release-directory I get the behavior I would expect memory consumption increases upto ~1GB then goes down to ~27MB (after the 1. GC.collect) up again and then down to ~27MB again (for the 2. GC.collect).

Now when running it in the debugger memory consumption for doing this goes up to ~1GB and for the 1.GC.collect memory consumption stays exactly where it was then goes up to 1.6GB withing the 2nd for-loop takes ages there and then I finally get an OutOfMemoryException within the 2nd for-loop.

Questions

Why do I get this behavior in the debugger?
Shouldn't garbage collection work during debugging as well, am I missing some Info about the debugger?

Side notes

  • I'm using visual studio 2010 Express edition
  • I only call GC.Collect() for the specific testing purposes here to be sure that garbage collection should have taken place.(I don't plan to use it normally)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;    

namespace Tree
{
  class Program
  {
    static void Main(string[] args)
    {

      TreeNode root = new TreeNode(null); // the null-argument is the parent-node

      TreeNode node = root;

      for (int i = 0; i < 15000000; i++)
      {
        TreeNode child = new TreeNode(node); 
        node = child;
      }

      root.RemoveChild(root.Children[0] );
      node = root;
      GC.Collect();

      for (int i = 0; i < 15000000; i++)
      {
        TreeNode child = new TreeNode(node);
        node = child;
      }
      root.RemoveChild(root.Children[0]);
      node = root;

      GC.Collect();

      Console.ReadLine();
    }
  }
}

I only include the following code in case you want to test it for yourself, it's not really useful


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Tree
{
  class TreeNode
  {
    public TreeNode Parent { get; private set; }
    public List<TreeNode> Children { get; set; }

    public TreeNode(TreeNode parent)
    {
      // since we are creating a new node we need to create its List of children
      Children = new List<TreeNode>();

      Parent = parent;
      if(parent != null) // the root node doesn't have a parent-node
        parent.AddChild(this);
    }

    public TreeNode(TreeNode parent, List<TreeNode> children)
    {
      // since we are creating a new node we need to create its List of children
      Children = new List<TreeNode>();

      Parent = parent;
      if (parent != null) // the root node doesn't have a parent-node
        parent.AddChild(this);

      Children = children;
    }

    public void AddChild(TreeNode child)
    {
      Children.Add(child);
    }

    public void RemoveChild(TreeNode child)
    {
      Children.Remove(child);
    }

  }
}
Community
  • 1
  • 1
Jennifer Owens
  • 4,044
  • 5
  • 19
  • 22

1 Answers1

6

This is by design. The lifetime of an object reference in a method is extended to the end of the method when the debugger is attached. This is important to make debugging easy. Your TreeNode class keeps both a reference to its parent and its children. So any reference to a tree node keeps the entire tree referenced.

Including the child reference, it keeps the removed section of the tree referenced. While it is no longer in scope by the time you call GC.Collect(), it still exists in the method's stack frame. Scope is a language feature, not a runtime feature. Without a debugger, the jitter tells the garbage collector that the child reference is no longer live at the end of the for loop. So its referenced nodes can be collected.

Note that you won't get OOM when you set child explicitly to null:

  for (int i = 0; i < 15000000; i++)
  {
    TreeNode child = new TreeNode(node); 
    node = child;
    child = null;
  }

Do not write that kind of code, you've made a very artificial example.

Hans Passant
  • 922,412
  • 146
  • 1,693
  • 2,536
  • Great explanation thanks a lot! What specifically do you mean with "Do not write that kind of code, you've made a very artificial example". The tree-nodes are basically the initial version and I do want to use them (after adding content-functionality) so I wanted to make sure with that test-code that removing a subtree from a referenced tree and not having any reference on one of the subtree's nodes is enough to be sure that I'm not leaking memory and the GC is taking care of it. So do you mean the test-code is artificial or that specific way of removing a subtree? Best regards! – Jennifer Owens Apr 05 '11 at 00:58
  • Don't set local variables to null. – Hans Passant Apr 05 '11 at 01:01