为什么GC在递归函数中似乎失败?

时间:2016-04-22 19:01:51

标签: c# .net recursion garbage-collection

如果按原样运行它会很快执行并且不会占用内存。如果你取消注释不好,它会变慢并最终锁定,如果没有抛出内存异常。

为什么GC在递归函数中似乎失败了?

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

namespace Repo
{
    class Program
    {
        static Random rng = new Random(42);
        static void Main(string[] args)
        {
            new Thread(Main2, 50 * 1024 * 1024).Start(); //Increase stack size
        }
        static void Main2()
        {
            //Bad(0, "test");
            var ls = Good(0, "test");
            while(ls.Any())
            {
                var v = ls.First();
                ls.AddRange(Good(v.Item1, v.Item2));
                ls.RemoveAt(0);
            }
        }
        class Foo
        {
            public byte[] data;
            public Foo(int size) { data = new byte[size]; }
        }
        static List<Tuple<int, string>> Good(int a, string b)
        {
            if (a >= 5000000)
                return new List<Tuple<int, string>>();
            Console.WriteLine("{0}", a);
            var ls = new List<Tuple<int, string>>();
            {
                var data = new byte[rng.Next(1024, 1024 * 20)]; //This line eats up all the memory
                ls.Add(Tuple.Create(a + 1, ASCIIEncoding.Default.GetString(data, 128, 64)));
            }
            return ls;
        }
        static void Bad(int a, string b)
        {
            if (a >= 5000000)
                return;
            Console.WriteLine("{0}", a);
            var ls = new List<Tuple<int, string>>();
            {
                var data = new byte[rng.Next(1024, 1024 * 20)]; //This line eats up all the memory
                ls.Add(Tuple.Create(a+1, ASCIIEncoding.Default.GetString(data, 128, 64)));
            }
            foreach(var v in ls)
            {
                Bad(v.Item1, v.Item2);
            }
            return;
        }
    }
}

3 个答案:

答案 0 :(得分:5)

Good()版本中,您只需分配ls一次,为其添加一个相当小的元组,并Good()返回给调用者。

Bad()版本中,Bad()的初始调用会产生对Bad()的许多递归调用,这反过来会产生许多自己的递归调用。您在ls的每次迭代中不断创建Bad()的新实例,并继续添加新元组。您的内存分析器应该向您显示List<Tuple<int, string>>案例中Bad()类型使用的字节数更多。它不是byte[] data,它应该超出范围。

在给定的递归调用完成之前,无法收集对ls的引用。

答案 1 :(得分:5)

JIT可以执行生命周期分析并确定可以在超出范围之前收集局部变量,因此并不是您的数组必须以其堆栈框架为根。

但是,在调试版本或调试器下运行时,GC更加保守(如果您想在调试会话期间检查值)。在调试器之外运行一个发布版本,你会看到更加渐进的内存增加,这可以通过递归调用中列表和元组的生动引用来解释。

答案 2 :(得分:2)

GC不会清理仍然引用的对象,并且在递归链中,您要创建的对象不会被引用,直到递归方法达到其结束条件并开始渗透为止到第一个电话。所以基本上你看到了一个很好的例子,说明GC如何清理它仍然在使用的任何东西(例如递归链中的祖先)。

如果您对如何深入完成.NET和内存处理感到好奇,可以尝试使用.NET Memory Profiler(http://memprofiler.com/)或类似工具之类的工具。