关于c#:垃圾孤立对象(树节点)的集合适用于“ release-exe”,但不适用于VS-debugger

Garbage Collection of orphaned objects (tree-nodes) works for the “release-exe” but not in VS-debugger

情况

根据这个公认的答案,如果" GC'看到'一个2个或更多对象的循环引用,而其他任何对象或永久性GC句柄都未引用这些对象,则将收集这些对象。"

我想知道垃圾回收是否适用于一个甚至都没有内容的超简单树形结构,只有带有父引用和子引用的树节点。

想象一下,您创建了一个根节点,向其添加了一个子节点,然后向该子节点添加了一个子节点,依此类推,实际上不是一棵树,而是更像一个列表(每个节点最多有一个子节点和一个父节点)。

如果按照我上面的答案,然后删除根的子级以及该子级子树中对节点的所有引用,则垃圾收集器应清理该子树。

问题描述

如果您查看下面的测试代码中的Main方法,则从Release目录运行exe时,我会发现我的行为是,我预计内存消耗将增加到?1GB,然后下降到?27MB(在1.之后)。 GC.collect)再次升高,然后再次降至?27MB(对于2. GC.collect)。

现在在调试器中运行它时,执行此操作的内存消耗高达?1GB,对于1.GC.collect,内存消耗精确地保持在原来的位置,然后随着第二个for循环的使用而上升至1.6GB,然后在那里最终在第二个for循环中获得OutOfMemoryException。

问题

为什么在调试器中出现此行为?
调试期间也不应该进行垃圾回收,我是否缺少一些有关调试器的信息?

旁注

  • 我正在使用Visual Studio 2010 Express版
  • 我仅出于特定的测试目的而调用GC.Collect(),以确保应该进行垃圾收集。(我不打算正常使用它)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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();
    }
  }
}

我仅包含以下代码,以备您自己测试时使用,它并不是真的有用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
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);
    }

  }
}

这是设计使然。 附加调试器后,方法中的对象引用的生存期将延长到方法的末尾。 这对于简化调试很重要。 您的TreeNode类同时保留对其父级和子级的引用。 因此,任何对树节点的引用都会引用整个树。

包括子引用在内,它将保留树的已删除部分的引用。 虽然在您调用GC.Collect()时,它的作用域已不再存在,但它仍然存在于方法的堆栈框架中。 范围是一种语言功能,而不是运行时功能。 如果没有调试器,则抖动会告知垃圾回收器,子引用不再存在于for循环的末尾。 因此可以收集其引用的节点。

请注意,将child显式设置为null时,您不会获得OOM:

1
2
3
4
5
6
  for (int i = 0; i < 15000000; i++)
  {
    TreeNode child = new TreeNode(node);
    node = child;
    child = null;
  }

不要编写这种代码,您已经做了一个非常人为的示例。