Garbage Collector Behavior for Destructor

Viewed 262

I have a simple class that is defined as below.

public class Person
{
    public Person()
    {

    }

    public override string ToString()
    {
        return "I Still Exist!";
    }

    ~Person()
    {
        p = this;

    }
    public static Person p;
}

In Main method

    public static void Main(string[] args)
    {
        var x = new Person();
        x = null;

        GC.Collect();
        GC.WaitForPendingFinalizers();
        Console.WriteLine(Person.p == null);

    }

Is garbage collector supposed to main reference for Person.p and when exactly will the destructor be called?

1 Answers

The thing you are missing here is that the compiler is extending the lifetime of your x variable until the end of the method in which it is defined - that's just something the compiler does - but it only does it for a DEBUG build.

If you change the code so that the variable is defined in a separate method, it will work as you expect.

The output of the following code is:

False
True

And the code:

using System;

namespace ConsoleApp1
{
    class Finalizable
    {
        ~Finalizable()
        {
            _extendMyLifetime = this;
        }

        public static bool LifetimeExtended => _extendMyLifetime != null;

        static Finalizable _extendMyLifetime;
    }

    class Program
    {
        public static void Main()
        {
            test();

            Console.WriteLine(Finalizable.LifetimeExtended); // False.

            GC.Collect();
            GC.WaitForPendingFinalizers();

            Console.WriteLine(Finalizable.LifetimeExtended); // True.
        }

        static void test()
        {
            new Finalizable();
        }
    }
}

So basically your understanding was correct, but you didn't know that the sneaky compiler was going to keep your variable alive until after you called GC.Collect() - even if you explicitly set it to null!

As I noted above, this only happens for a DEBUG build - presumably so you can inspect the values for local variables while debugging to the end of the method (but that's just a guess!).

The original code DOES work as expected for a release build - so the following code outputs false, true for a RELEASE build and false, false for a DEBUG build:

using System;

namespace ConsoleApp1
{
    class Finalizable
    {
        ~Finalizable()
        {
            _extendMyLifetime = this;
        }

        public static bool LifetimeExtended => _extendMyLifetime != null;

        static Finalizable _extendMyLifetime;
    }

    class Program
    {
        public static void Main()
        {
            new Finalizable();

            Console.WriteLine(Finalizable.LifetimeExtended); // False.

            GC.Collect();
            GC.WaitForPendingFinalizers();

            Console.WriteLine(Finalizable.LifetimeExtended); // True iff RELEASE build.
        }
    }
}

As an addendum: Note that if you do something in the finalizer for a class that causes a reference to the object being finalized to be reachable from a program root, then that object will NOT be garbage-collected unless and until that object is no longer referenced.

In other words, you can give an object a "stay of execution" via the finalizer. This is generally held to be a bad design, though!

For example, in the code above, where we do _extendMyLifetime = this in the finalizer, we are creating a new reference to the object, so it will not now be garbage-collected until _extendMyLifetime (and any other reference) no longer references it.

Related