Unclear behavior by Garbage Collector while collec

2020-03-28 19:39发布

问题:

Till today I was thinking that members of reachable objects are also considered to be reachable.

But, today I found one behavior which creates a problem for us either when Optimize Code is checked or application is executed in Release Mode. It is clear that, release mode comes down to the code optimization as well. So, it seems code optimization is reason for this behavior.

Let's take a look to that 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() { }
 }

I simplified code a bit. Actually, we are using our special delegate, not Action. But the behavior is same. This code is written in mind with "members of reachable objects are also considered to be reachable". But, that delegate will be collected by GC asap. And we have to pass it to some unmanaged library, which will use it for some time.

You can test demo by just adding that line to the Main method:

var p = new Demo();

I can understand the reason of that optimization, but what is the recommended way to prevent such case without creating another function which will use that variable myDelWithMethod which will be called from some place? One, option I found that, it will work if I will set myDelWithMethod in the constructor like so:

myDelWithMethod = () => { };

Then, it won't be collected until owning instance is collected. It seems it can't optimize code in the same way, if lambda expression is setted as a value.

So, will be happy to hear your thoughts. Here are my questions:

  • Is it right that, members of reachable objects are also considered to be reachable?

  • Why it is not collected in case of lambda expression?

  • Any recommended ways to prevent collection in such cases?

回答1:

However strange this would sound, JIT is able to treat an object as unreachable even if the object's instance method is being executed - including constructors.

An example would be the following code:

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...");
   }
}

that may print:

615323
Killing...
Am I dead?

This is because of inlining and Eager Root Collection technique - DoSomethingElse method do not use any SomeClass fields, so SomeClass instance is no longer needed after LINE 2.

This happens to code in your constructor. After // ... Pass it to unmanaged library line your Demo instance becomes unreachable, thus its field myDelWithMethod. This answers the first question.

The case of empty lamba expression is different because in such case this lambda is cached in a static field, always reachable:

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)));
    }
}

Regarding recommended ways in such scenarios, you need to make sure Demo has lifetime long enough to cover all unmanaged code execution. This really depends on your code architecture. You may make Demo static, or use it in a controlled scope related to the unmanaged code scope. It really depends.