循环变量没有被收集

时间:2011-05-24 03:48:34

标签: c# loops scope

我有一个循环变量似乎没有收集垃圾(根据Red-Gate ANTS内存分析器),尽管已超出范围。

代码看起来像这样:

while (true)
{
    var item = blockingQueue.dequeue(); // blocks until an item is added to blockingQueue
    // do something with item
}

据我所知,在item返回之前,对blockingQueue.dequeue()的引用仍然存在。这是预期的行为,还是内存分析器中的错误?

其次,如果这是预期的行为,我将如何强制item在循环体的末尾收集?将其设置为null似乎不会导致它被收集。这很重要,因为队列可能会长时间阻塞,item引用相当大的对象树。

请注意,探查器的文档说在执行内存快照之前执行了GC,并且引用不在终结器队列中。

我能够使用代码here重现相同的问题。

更新

要点中的代码略有缺陷,因为它合法地保留在GetFoo()中的引用中。更改后,现在可以在明确设置为null时收集 对象。但是,我相信汉斯的回答解释了我在实际代码中看到的情况。

6 个答案:

答案 0 :(得分:7)

抖动优化器可能是此问题的根源。这是一个例子:

class Program {
    static void Main(string[] args) {
        while (true) {
            var input = Console.ReadLine();
            Console.WriteLine(input);
            input = null;
        }
    }
}

生成此机器代码:

            while (true) {
                var input = Console.ReadLine();
00000000  push        ebp                    ; setup stack
00000001  mov         ebp,esp 
00000003  push        esi  
00000004  call        6E0208F0               ; Console.In property getter
00000009  mov         ecx,eax 
0000000b  mov         eax,dword ptr [ecx] 
0000000d  call        dword ptr [eax+64h]    ; TextReader.ReadLine()
00000010  mov         esi,eax                ; assign input variable
                Console.WriteLine(input);
00000012  call        6DB7BE38               ; Console.Out property getter
00000017  mov         ecx,eax
00000019  mov         edx,esi
0000001b  mov         eax,dword ptr [ecx] 
0000001d  call        dword ptr [eax+000000D8h] ; TextWriter.WriteLine()
00000023  jmp         00000004               ; repeat, note the missing null assigment

esi寄存器存储输入变量。请注意它永远不会设置为null,它始终存储对最后输入的字符串的引用。优化器已删除null赋值语句。垃圾收集器从抖动中获取生命周期提示,它会说该引用在循环期间是活动的。

问题发生在第二次和后续的传递中,当你从未输入内容时,ReadLine()将阻塞(类似于你的阻塞队列),并且esi寄存器值继续引用该字符串。它永远不会在循环期间被垃圾收集,至少在重新分配之前是这样。

对此没有干净的解决方法。这是一个丑陋的:

    [MethodImpl(MethodImplOptions.NoInlining)]
    public static void NullReference<T>(ref T obj) where T : class {
        obj = null;
    }

并使用:

        while (true) {
            var input = Console.ReadLine();
            Console.WriteLine(input);
            NullReference(ref input);
        }

答案 1 :(得分:1)

在调用Dequeue之前,item的值是否已被覆盖且仍在使用中?您可以做的最好的事情是将其设置为null,调用GC.Collect(),但是您不能保证收集该变量,也无法强制收集它,所以为什么要这么麻烦?

答案 2 :(得分:0)

while (true)
{
    {
        var item = blockingQueue.dequeue(); // blocks until an item is added to blockingQueue
        // do something with item
    }
    // do others that might be blocking for a long time
}

我怀疑将它封闭在一个区块中可能会有效。如果它是一次性的,你可以

while (true)
{
    using (var item = blockingQueue.dequeue(); 
    {
        // do something with item
    }
    // do others that might be blocking for a long time
}

我可能误解了你,但这是处理另一种情况的另一种可能性:

while (true)
{
    var item = null;
    item = blockingQueue.dequeue(); // blocks until an item is added to blockingQueue
    // do something with item
    item = null;
}

答案 3 :(得分:0)

如果您已完成该项目,则可以在循环体的末尾释放对它的引用:

item = null;

就垃圾收集而言,无论项目有多大,如果没有其他引用并且垃圾收集器没有收集它,那么垃圾收集器认为它还不需要收集。

让垃圾收集器完成它的工作。它将在适当的时候收集东西,并且它将有效地进行收集,在记忆和时间上进行权衡。

答案 4 :(得分:0)

我认为问题是item永远不会超出范围,直到循环结束。 GC不够智能,无法识别item中的值在被覆盖之前不会被使用,因此无法收集它。

完成后将其设置为null将删除最后一个引用并允许收集您的对象。

答案 5 :(得分:0)

以下两个代码片段产生相同的il:

int i = 0;
System.Object x;
while(i < 100){
    x = new System.Object();
    System.Console.WriteLine(x.ToString());
    i++;
}

现在尝试依赖词法作用域来释放x中的本地引用:

int i = 0;
while(i < 100){
    System.Object x = new System.Object();
    System.Console.WriteLine(x.ToString());
    i++;
}

两种情况下的结果相同。当循环的迭代结束时,持有名为x的ref的本地不会为空。即使我们未能分支到循环的开始,本地永远设置为null。相反,当机会出现时,编译器将重用此局部变量槽。

如果将x显式设置为null,即使您有优化标志,编译器也会发出il以将local设置为null。如果这是优化的,那就是在JIT中而不是静态编译器。