【问题标题】:Loop variable not getting collected未收集循环变量
【发布时间】:2011-05-24 03:48:33
【问题描述】:

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

代码如下所示:

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

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

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

注意,分析器的文档说,在拍摄内存快照之前执行 GC,并且引用不在终结器队列中。

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

更新

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

【问题讨论】:

  • 还有其他的参考吗?
  • 你能显示item的所有用法吗?
  • 分析器显示来自 GC 根的对象的引用,这就是其中之一。我在 github 上发布的示例显示了问题,并且 only 引用在循环内。
  • @Simon:我之前说过但删除了它,但是你用来重现这个的代码有问题。您使用 WinForms 应用程序中的控制台,更重要的是,您锁定并等待您的 UI 线程。问题是您的程序甚至不再运行,因为您正在等待一些不存在的线程来获取和释放锁。
  • 您是否有理由关心该项目何时被 GCed?还要确保你是在发布模式而不是调试模式下编译,在调试中,局部变量的生命周期通常会延长,以使调试更容易。

标签: c# loops scope


【解决方案1】:

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

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 寄存器存储 input 变量。请注意它是如何永远不会设置回 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);
        }

【讨论】:

    【解决方案2】:

    直到Dequeue被调用,那么item的值还没有被覆盖并且还在使用中对吗?您能做的最好的事情就是将其设置为 null,即调用 GC.Collect(),但不能保证收集该变量,也无法强制收集它,所以何必呢?

    【讨论】:

    • 我打扰了,因为在我的原因中,出队可能需要很长时间,并且该项目包含一个大型对象树的引用。在这一点上我能做的最好的事情是调用item.Dispose() 并希望它删除对其对象的引用(引用的类是第三方 GUI 组件,所以我无法控制它)。
    【解决方案3】:
    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;
    }
    

    【讨论】:

      【解决方案4】:

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

      item = null;
      

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

      让垃圾收集器完成它的工作。它会在适当的时候收集东西,而且它会高效地收集东西,权衡内存和时间。

      【讨论】:

        【解决方案5】:

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

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

        【讨论】:

          【解决方案6】:

          以下两个代码片段产生相同的 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 的本地不会为空。即使我们未能分支到循环的开头,本地也从不设置为空。相反,当机会出现时,编译器会重用这个局部变量槽。

          如果您将 x 显式设置为 null,即使您启用了优化标志,编译器也会发出 il 以将本地设置为 null。如果这被优化了,它会发生在 JIT 而不是静态编译器中。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 2013-05-06
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2012-12-28
            • 1970-01-01
            相关资源
            最近更新 更多