Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Need explanation of how an object can get garbage-collected when a method is executing on it

In his blog, When does an object become available for garbage collection?, Reymond Chen writes that

An object can become eligible for collection during execution of a method on that very object.

Also, Curt Nichols demonstrates the same point through this example

public class Program
    {
        static void Main(string[] args)
        {
            new TestClass().InstanceMethod();

            Console.WriteLine("End program.");
            Console.ReadLine();
        }
    }

    public sealed class TestClass
    {
        private FileStream stream;

        public TestClass()
        {
            Console.WriteLine("Ctor");

            stream = new FileStream(Path.GetTempFileName(), FileMode.Open);
        }

        ~TestClass()
        {
            Console.WriteLine("Finializer");

            stream.Dispose();
        }

        public void InstanceMethod()
        {
            Console.WriteLine("InstanceMethod");

            StaticMethod(stream);
        }

        private static void StaticMethod(FileStream fs)
        {
            GC.Collect();
            GC.WaitForPendingFinalizers();

            Console.WriteLine("StaticMethod"); 
            var len = fs.Length;
        }
    }

The output is as expected -

Ctor
InstanceMethod
Finalizer
StaticMethod

ObjectDisposedException is thrown

In this example, I am not able to understand, how GC could collect the temporary TestClass object since its member stream was being referred to by the StaticMethod.

Yes, Raymond states that

GC is not about tracing roots but about removing objects that are no more in use

However, in this example TestClass object is still being used, isn't it?

Please explain how GC is right in collecting TestClass object in this case? Also, more importantly, how should developers safeguard against these situations?

like image 564
Unmesh Kondolikar Avatar asked Feb 02 '23 20:02

Unmesh Kondolikar


2 Answers

I am not able to understand, how GC could collect the temporary TestClass object since its member stream was being referred to by the StaticMethod.

StaticMethod is in fact not holding onto a reference the TestClass instance's stream member - it's holding onto a reference to (as far as the GC is concerned) some FileStream object on the heap.

Note the default pass-by-value semantics of C#. In this statement:

StaticMethod(stream);

A copy of the value of the stream field is passed as an argument to the static method. FileStream is a reference-type and the value of a reference-type expression is a reference. Hence, a reference to a FileStream object on the heap is passed to the method, not (as you appear to be thinking) a reference / interior reference to a TestClass object

The fact that this FileStream object is still reachable when GC.Collect is called does not result in making the TestClass object (that created it and has a reference to it through a field) also reachable. "Live" objects make other objects live by referencing them, not by being referred to by them.

Assuming optimizations that result in unneeded references being aggressively "popped off", let's look at the reachability of the created TestClass object.

  • Main doesn't need a reference to it after it calls:
  • InstanceMethod, which in turn doesn't need the implicitly passed this reference after it dereferences it to read the stream field. The object becomes eligible for collection at this point. It then calls:
  • StaticMethod, which in turn (as mentioned earlier) isn't holding to a reference to the object at all.

Consequently, the TestClass object isn't needed right after after its stream field is read, and is most certainly eligible for collection while StaticMethod is executing.

like image 121
Ani Avatar answered Feb 05 '23 09:02

Ani


This seems to be because when the instance of TestClass calls the StaticMethod it passes by reference the stream object, rather than letting the StaticMethod use the this keyword to access the instance field itself (which would keep a reference to the instance alive).

So as soon as it is passed to the static method by reference the instance can be garbage collected and the stream object disposed of in the Finalizer causing the ObjectDisposedException to be thrown.

I think Curt Nichols explains it pretty well in the blog

Why does this throw? Well, the last live reference to the instance was lost when LongRunningMethod passed _input to Helper. Essentially we've again exported the field value from the instance and no longer hold a reference to the instance, allowing the GC to finalize it. Helper is left holding a reference to an object that has been finalized.

like image 42
Peter Kelly Avatar answered Feb 05 '23 09:02

Peter Kelly