【问题标题】:Do lambdas assigned to an event prevent garbage collection of the owning object?分配给事件的 lambda 是否会阻止对拥有对象进行垃圾收集?
【发布时间】:2012-06-27 13:00:30
【问题描述】:

假设您有一个带有事件属性的类。如果您在本地上下文中实例化此类,而没有外部引用,那么将 lambda 表达式分配给事件是否会阻止实例被垃圾回收?

{
    var o = new MyClass();
    o.MyClassEvent += (args) => {};
}
// Will 'o' be eligible for garbage collection here?

【问题讨论】:

标签: c# events lambda garbage-collection


【解决方案1】:

不,o 将被释放,lambda 函数也将被释放。其他任何地方都没有对o 的引用,因此没有理由不释放它。

【讨论】:

    【解决方案2】:

    简短回答,不,o 将被释放。不用担心。

    答案稍长:

    您的代码或多或少做了以下事情:

    1. 在该线程上创建一些本地存储以引用新的 MyClass (o)。
    2. 创建新的 MyClass
    3. 将新 MyClass 的引用存储在 o
    4. 从 lambda 创建一个新委托。
    5. 将该委托分配给事件(o 现在具有对委托的引用)。

    在此期间的任何时候,GC 都可能会停止线程,然后检查哪些对象是有根的或没有根的。根是静态对象或线程本地存储中的对象(我的意思是给定线程执行中的局部变量,由堆栈实现,而不是本质上是静态形式的“线程本地存储”)。有根对象是根、它们引用的对象、它们引用的对象等等。有根对象不会被收集,其余的会被收集(除非我们暂时忽略一些与终结器有关的额外内容)。

    在创建 MyClass 对象之后到目前为止,它还没有被线程的本地存储植根。

    在创建委托对象之后到目前为止,它没有被线程的本地存储或 MyClass 对它的引用所植根。

    现在,接下来会发生什么?

    这取决于。 lambda 不会让 MyClass 保持活动状态(MyClass 保持活动状态,但是当 MyClass 消失时,lambda 也会保持活动状态)。这还不足以回答“'o' 是否有资格在这里进行垃圾收集?”不过。

    void Meth0()
    {
        var o = new MyClass();
        o.MyClassEvent += (args) => {};
    }//o is likely eligible for collection, though it doesn't have to be.
    
    void Meth1()
    {
      int i = 0;
      {
        var o = new MyClass();
        o.MyClassEvent += (args) => {};
      }//o is likely not eligible for collection, though it could be.
      while(i > 100000000);//just wasting time
    }
    
    void Meth2()
    {
      {
        var o = new MyClass();
        o.MyClassEvent += (args) => {};
        int i = 0;//o is likely eligible for collection, though it doesn't have to be.
        while(i > 100000000);//just wasting time
      }
    }
    
    void Meth3()
    {
      {
        var o = new MyClass();
        o.MyClassEvent += (args) => {};
        var x0 = new MyClass();//o is even more likely eligible for collection, though it doesn't have to be.
        var x1 = new MyClass();//o is even more likely eligible for collection, though it doesn't have to be.
        var x2 = new MyClass();//o is even more likely eligible for collection, though it doesn't have to be.
        var x3 = new MyClass();//o is even more likely eligible for collection, though it doesn't have to be.
        var x4 = new MyClass();//o is even more likely eligible for collection, though it doesn't have to be.
        var x5 = new MyClass();//o is even more likely eligible for collection, though it doesn't have to be.
        var x6 = new MyClass();//o is even more likely eligible for collection, though it doesn't have to be.
        var x7 = new MyClass();//o is even more likely eligible for collection, though it doesn't have to be.
        var x8 = new MyClass();//o is even more likely eligible for collection, though it doesn't have to be.
        var x9 = new MyClass();//o is even more likely eligible for collection, though it doesn't have to be.
        var x10 = new MyClass();//o is even more likely eligible for collection, though it doesn't have to be.
        var x11 = new MyClass();//o is even more likely eligible for collection, though it doesn't have to be.
        int i = 0;
        while(i > 100000000);//just wasting time
      }
    }
    

    Meth0 看似最简单,但实际上并非如此,所以我们会回到它。

    在 Meth1 中,实现可能保留本地存储,因为它不再需要,所以虽然 o 不在范围内,但实现仍然有正在使用的本地存储。

    在 Meth2 中,实现可以使用与 oi 相同的本地存储,因此即使它仍在范围内,它也有资格收集(“在范围内" 表示程序员可以选择用它做某事,但他或她没有,编译后的代码不需要这样的概念)。

    Meth3 更有可能重复使用存储空间,因为它的额外临时使用使得该实施与一开始就将所有存储空间搁置相比更有意义。

    这些事情都必须变成这样。 Meth2 和 Meth3 都可以在开始时预留该方法所需的所有存储空间。 Meth1 可以重用存储空间,因为重新排序分配的 io 没有区别。

    Meth0 更复杂,因为它可能取决于调用方法接下来对本地存储执行的操作,而不是当时是否进行清理(两者都是合法的实现)。 IIRC 总是对当前的实现进行清理,但我不确定,反正也没关系。

    总而言之,范围不是相关的东西,而是编译器和后来的 JITter 是否可以并且确实使用与对象相关的本地存储。甚至可以在调用对象的方法和属性之前清理对象(如果这些方法和属性不使用 this 或对象的任何字段,因为如果对象已经已删除!)。

    【讨论】:

      【解决方案3】:

      当运行时离开该代码块时,不再有对该对象的引用,因此它会被垃圾回收。

      【讨论】:

      • 你怎么知道本地存储中没有对它的引用?
      • 是否有引用无关紧要。事实上,lambda 可能是一个堆分配的对象,并引用了该对象,所以你的说法是错误的。对 GC 来说重要的是对象是否可达
      【解决方案4】:

      这取决于您所说的“本地上下文”到底是什么意思。比较:

      static void highMem()
      {
          var r = new Random();
          var o = new MyClass();
          o.MyClassEvent += a => { };
      }
      static void Main(string[] args)
      {
          highMem();
          GC.Collect(); //yes, it was collected
      }
      

      收件人:

      static void Main(string[] args)
      {
          var r = new Random();
          {
              var o = new MyClass();
              o.MyClassEvent += a => { };
          }
          GC.Collect(); //no, it wasn't collected
      }
      

      在我的环境中(调试版本,VS 2010,Win7 x64),第一个符合 GC 条件,第二个没有(通过让 MyClass 占用 200MB 内存并在任务管理器中检查),即使它超出了范围。我想这是因为编译器在方法的开头声明了所有局部变量,所以对于 CLR,o 并没有超出范围,即使你不能在 C# 代码中使用它。

      【讨论】:

      • o 超出范围,但范围与垃圾收集无关,实时引用是。如果在引用仍在范围内时使用了引用的堆栈空间,则可以收集它。在某些情况下,它甚至可以在操作完成之前收集。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-12-20
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-03-18
      • 2012-12-29
      相关资源
      最近更新 更多