Warning: file_get_contents(/data/phpspider/zhask/data//catemap/2/csharp/302.json): failed to open stream: No such file or directory in /data/phpspider/zhask/libs/function.php on line 167

Warning: Invalid argument supplied for foreach() in /data/phpspider/zhask/libs/tag.function.php on line 1116

Notice: Undefined index: in /data/phpspider/zhask/libs/function.php on line 180

Warning: array_chunk() expects parameter 1 to be array, null given in /data/phpspider/zhask/libs/function.php on line 181
C# 为什么GC在递归函数中失败?_C#_.net_Recursion_Garbage Collection - Fatal编程技术网

C# 为什么GC在递归函数中失败?

C# 为什么GC在递归函数中失败?,c#,.net,recursion,garbage-collection,C#,.net,Recursion,Garbage Collection,如果按原样运行,它将执行得相当快,并且不会占用内存。若您取消注释bad,它将变慢,若并没有抛出内存不足异常,它最终将被锁定 为什么GC在递归函数中失败 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; namespace Repo { class Progra

如果按原样运行,它将执行得相当快,并且不会占用内存。若您取消注释bad,它将变慢,若并没有抛出内存不足异常,它最终将被锁定

为什么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;
        }
    }
}
使用系统;
使用System.Collections.Generic;
使用System.Linq;
使用系统文本;
使用系统线程;
使用System.Threading.Tasks;
名称空间回购
{
班级计划
{
静态随机rng=新随机(42);
静态void Main(字符串[]参数)
{
新线程(Main2,50*1024*1024).Start();//增加堆栈大小
}
静态无效Main2()
{
//坏(0,“测试”);
var ls=良好(0,“测试”);
while(ls.Any())
{
var v=ls.First();
ls.添加范围(良好(第1项、第2项);
ls.RemoveAt(0);
}
}
福班
{
公共字节[]数据;
公共Foo(int size){data=新字节[size];}
}
静态列表良好(整数a、字符串b)
{
如果(a>=5000000)
返回新列表();
Console.WriteLine(“{0}”,a);
var ls=新列表();
{
var data=new byte[rng.Next(1024,1024*20)];//此行会占用所有内存
Add(Tuple.Create(a+1,ascienceoding.Default.GetString(data,128,64));
}
返回ls;
}
静态无效错误(整数a、字符串b)
{
如果(a>=5000000)
返回;
Console.WriteLine(“{0}”,a);
var ls=新列表();
{
var data=new byte[rng.Next(1024,1024*20)];//此行会占用所有内存
Add(Tuple.Create(a+1,ascienceoding.Default.GetString(data,128,64));
}
foreach(ls中的var v)
{
不良(第1项、第2项);
}
返回;
}
}
}
Good()
版本中,分配
ls
一次,向其添加一个相当小的元组,然后
Good()
返回调用方

Bad()
版本中,
Bad()
的初始调用会产生许多对
Bad()
的递归调用,而这些调用又会产生许多自己的递归调用。在
Bad()
的每次迭代中,您不断创建
ls
的新实例,并不断向其中添加新元组。内存分析器应该向您显示,在
Bad()
案例中,类型
列表使用的字节要多得多。不是
字节[]数据
,它确实应该超出范围


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

GC不会清理仍然被引用的对象,并且使用递归链,在递归方法达到其结束条件并开始渗透回第一次调用之前,您创建的对象不会被取消引用。因此,本质上,您看到了一个很好的示例,说明GC不会清理它认为仍在使用的任何东西(例如递归链中的祖先)


如果您对.NET和内存处理是如何深入进行的感到好奇,可以尝试使用.NET内存分析器()或类似的工具。

JIT可以执行生存期分析,并确定可以在局部变量超出范围之前收集它们,所以数组不一定要以堆栈框架为根


但是在调试生成中或在调试器下运行时,GC更为保守(以防您希望在调试会话期间检查值)。在调试器之外运行发布版本,您将看到内存逐渐增加,这可以通过递归调用中对列表和元组的实时引用来解释。

探查器说它的Foo。它似乎不是您建议的列表。我运行这段代码是为了让我知道列表占用了多少内存,而实际上并不是很多。你说得对。对于repo,这个分析器向我显示内存正在被消耗,但不是如何被消耗的(我得到一个空列表。尽管它说我使用了大于1gb的内存,这比我链接的列表片段要多)。我没有意识到我意外地点击了撤销太多次,并且没有包括foo。我分析的主要应用程序显示,第三方HTML库是问题所在(我称之为foo),它在递归过程中不在作用域内。你是说当我在错误中点击foreach循环时,仍然有对foo的引用吗?因为不应该是这样,而且分析器会让它看起来像是这样,除非我是盲人,否则我看不到
Foo
在代码中的任何地方实际被使用或引用。忘记Foo吧,在
bad
函数中创建的所有内容(ls和数据以及堆栈)都不会被释放,直到大型递归展开。这是永远不会发生的,因为在它完成之前,内存就用完了,数据。。在每个堆栈中,帧是不同的。您正在尝试创建5000000个ls和5000000个数据,。。。。把它们都留在记忆中once@acidzombie24这一点的主要好处不是Foo的问题,但只要调用Bad方法,它就会启动一个递归链,在递归完成并开始返回链之前,它无法清理链中的任何数据。在这种情况发生之前,您的内存不足。一种考虑方法是,整个递归调用链必须能够同时放入内存,否则您将遇到这种情况,因为GC无法清理链中的任何内容,直到它到达最后一点,在这一点上,它可以在过滤时进行清理。对
Bad()的每个递归调用
将保留对
v.Item2
的引用,因为它被传递到
Bad()
。这将使对
v
的引用保持活动状态,因此
ls
,这意味着