垃圾收集器在收集可达对象的实例属性或字段时的行为不明确

Unclear behavior by Garbage Collector while collecting instance properties or fields of reachable object

直到今天我还在想可达对象的成员也被认为是可达的。

但是,今天我发现了一种行为,当 Optimize Code 被选中时 或应用程序正在在释放模式中执行。很明显,发布模式也归结为代码优化。所以,代码优化似乎是造成这种行为的原因。

让我们看一下代码:

 public class Demo
 {
     public Action myDelWithMethod = null;

     public Demo()
     {
         myDelWithMethod = new Action(Method);

         // ... Pass it to unmanaged library, which will save that delegate and execute during some lifetime 

         // Check whether object is alive or not after GC
         var reference = new WeakReference(myDelWithMethod, false);

         GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced, true);
         GC.WaitForPendingFinalizers();
         GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced, true);

         Console.WriteLine(reference.IsAlive);
         // end - Check whether object is alive or not after GC
     }

     private void Method() { }
 }

我稍微简化了代码。实际上,我们使用的是我们的特殊代表,而不是 Action。但是行为是一样的。这段代码是用"members of reachable objects are also considered to be reachable"写的。但是,该委托将由 GC 尽快收集。我们必须将它传递给一些非托管库,这些库将使用它一段时间。

您只需将该行添加到 Main 方法即可测试演示:

var p = new Demo();

我能理解优化的原因,但是在不创建另一个函数的情况下防止这种情况的推荐方法是什么,该函数将使用将从某个地方调用的变量myDelWithMethod一,我发现的选项,如果我在构造函数中设置 myDelWithMethod 就可以了:

myDelWithMethod = () => { };

然后,直到拥有的实例被收集,它才会被收集。如果将 lambda 表达式设置为一个值,它似乎无法以相同的方式优化代码。

所以,很高兴听到您的想法。这是我的问题:

无论这听起来多么奇怪,JIT 能够将对象视为不可访问,即使对象的实例方法正在执行 - 包括构造函数。

一个例子是下面的代码:

static void Main(string[] args)
{
   SomeClass sc = new SomeClass() { Field = new Random().Next() };
   sc.DoSomethingElse();
}
class SomeClass
{
   public int Field;
   public void DoSomethingElse()
   {
      Console.WriteLine(this.Field.ToString());
      // LINE 2: further code, possibly triggering GC
      Console.WriteLine("Am I dead?");
   }
   ~SomeClass()
   {
      Console.WriteLine("Killing...");
   }
}

可能打印:

615323
Killing...
Am I dead?

这是因为 内联Eager Root 收集技术 - DoSomethingElse 方法不使用任何 SomeClass 字段,因此在 LINE 2.

之后不再需要 SomeClass 实例

这恰好发生在您的构造函数中的代码中。在 // ... Pass it to unmanaged library 行之后,您的 Demo 实例变得不可访问,因此它的字段 myDelWithMethod。这回答了第一个问题。

空 lamba 表达式的情况不同,因为在这种情况下此 lambda 缓存在静态字段中,始终可访问:

public class Demo
{
    [Serializable]
    [CompilerGenerated]
    private sealed class <>c
    {
        public static readonly <>c <>9 = new <>c();
        public static Action <>9__1_0;
        internal void <.ctor>b__1_0()
        {
        }
    }

    public Action myDelWithMethod;
    public Demo()
    {
        myDelWithMethod = (<>c.<>9__1_0 ?? (<>c.<>9__1_0 = new Action(<>c.<>9.<.ctor>b__1_0)));
    }
}

关于此类场景中的推荐方式,您需要确保 Demo 的生命周期足够长以涵盖所有非托管代码执行。这实际上取决于您的代码架构。您可以使 Demo 静态,或在与非托管代码范围相关的受控范围内使用它。这真的取决于。