如果按原样运行它会很快执行并且不会占用内存。如果你取消注释不好,它会变慢并最终锁定,如果没有抛出内存异常。
为什么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;
}
}
}
答案 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/)或类似工具之类的工具。