Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can anyone explain this finalisation behaviour

Whilst 'investigating' finalisation (read: trying stupid things) I stumbled across some unexpected behaviour (to me at least).

I would have expected the Finalise method to not be called, whereas it gets called twice

class Program
{
    static void Main(string[] args)
    {
        // The MyClass type has a Finalize method defined for it
        // Creating a MyClass places a reference to obj on the finalization table.
        var myClass = new MyClass();

        // Append another 2 references for myClass onto the finalization table.
        System.GC.ReRegisterForFinalize(myClass);
        System.GC.ReRegisterForFinalize(myClass);
        // There are now 3 references to myClass on the finalization table.

        System.GC.SuppressFinalize(myClass);
        System.GC.SuppressFinalize(myClass);
        System.GC.SuppressFinalize(myClass);

        // Remove the reference to the object.
        myClass = null;

        // Force the GC to collect the object.
        System.GC.Collect(2, System.GCCollectionMode.Forced);

        // The first call to obj's Finalize method will be discarded but
        // two calls to Finalize are still performed.
        System.Console.ReadLine();
    }
}

class MyClass
{
    ~MyClass()
    {
        System.Console.WriteLine("Finalise() called");
    }
}

Could anyone explain whether this behaviour is intentional and if so why?

This above code was compiled in x86 debug mode and running on the CLR v4.

Many thanks

like image 221
Rich O'Kelly Avatar asked Nov 04 '11 14:11

Rich O'Kelly


3 Answers

I do not know what is causing the bizarre behaviour. However, since you are in violation of the documented usage of the method, anything can happen. The documentation for ReRegisterForFinalize says:

Requests that the system call the finalizer for the specified object for which SuppressFinalize has previously been called.

You did not previously call SuppressFinalize before you called ReRegisterForFinalize. The documentation does not say what happens in that situation, and in fact, apparently something really weird happens.

Unfortunately, that same documentation page then goes on to show an example in which ReRegisterForFinalize is called on an object for which SuppressFinalize has not been called.

This is a bit of a mess. I'll take it up with the documentation manager.

The moral of the story is, of course, if it hurts when you violate the rules described in the documentation then stop violating them.

like image 186
Eric Lippert Avatar answered Sep 30 '22 02:09

Eric Lippert


I can guess... and this really is only a guess. As Eric says, don't break the rules like this :) This guess is only for the sake of idle speculation and interest.

I suspect that there are two data structures involved:

  • A finalization queue
  • The object's header

When the GC notices that an object is eligible for garbage collection, I suspect it checks the object's header and adds the reference to the finalization queue. Your calls to SuppressFinalization are preventing that behaviour.

Separately, the finalizer thread runs through the finalization queue and calls the finalizer for everything it finds. Your calls to ReRegisterForFinalize are bypassing the normal way that the reference ends up on the queue, and adding it directly. SuppressFinalization isn't removing the reference from the queue - it's only stopping the reference from being added to the queue in the normal way.

All of this would explain the behaviour you're seeing (and which I've reproduced). It also explains why when I remove the SuppressFinalization calls I end up seeing the finalizer called three times - because in this case the "normal" path adds the reference to the finalization queue as well.

like image 36
Jon Skeet Avatar answered Sep 30 '22 01:09

Jon Skeet


Interesting datapoints:

  • mono 2.10.8.1 on linux doesn't call finalizer
  • mono 2.8 on linux doesn't call finalizer: http://ideone.com/J6pl4
  • mono 2.8.1 on Win32 doesn't call finalizer
  • mono 2.6.7 on Win32 doesn't call finalizer

  • .NET 3.5 on Win32 calls finalizer twice

Test code for reference:

class Program
{
    static void Main(string[] args)
    {
        // The MyClass type has a Finalize method defined for it
        // Creating a MyClass places a reference to obj on the finalization table.
        var myClass = new MyClass();

        // Append another 2 references for myClass onto the finalization table.
        System.GC.ReRegisterForFinalize(myClass);
        System.GC.ReRegisterForFinalize(myClass);
        // There are now 3 references to myClass on the finalization table.

        System.GC.SuppressFinalize(myClass);
        System.GC.SuppressFinalize(myClass);
        System.GC.SuppressFinalize(myClass);

        // Remove the reference to the object.
        myClass = null;

        // Force the GC to collect the object.
        System.GC.Collect(2, System.GCCollectionMode.Forced);

        // The first call to obj's Finalize method will be discarded but
        // two calls to Finalize are still performed.
    }
}

class MyClass
{
    ~MyClass()
    {
        System.Console.WriteLine("Finalise() called");
    }
}
like image 28
sehe Avatar answered Sep 30 '22 03:09

sehe