Can anyone explain this finalisation behaviour
Asked Answered
L

4

6

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

Landman answered 4/11, 2011 at 14:44 Comment(0)
H
10

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.

Healion answered 4/11, 2011 at 14:55 Comment(2)
Nice call, Jon. Looking at the CLI source that MS released a while back, there's an if statement in the internal ReRegisterForFinalize() method that says "if I've been marked as finalized, remove that mark. If not, just add me to the queue." So if you call ReReg twice without supressing first, the object just gets added to the queue twice. Then, suppress clears the bit three times (which does nothing, really, since the bit was never set to begin with!) In any case, brilliant speculation, as always.Susurrous
Thanks @Jon and dlev, this is what I suspected. It is however contrary to my previous assumed (and provably incorrect) mental model, which was that one method adds and another removes from the finalization table. It's obviously against the documented usage, but seems an oversight that if an object is re-registered for finalisation that shouldn't be it can never be suppressed, I was curious what the reason was - I assume performance reasons. sehe's investigations into how mono behaves is quite interesting, I suppose that's the beauty of undefined behavior in the spec!Duro
S
16

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.

Sibell answered 4/11, 2011 at 14:53 Comment(3)
If you move one of the SuppressFinalize calls before the two ReRegisterForFinalize, it is only called once. Basically, there must be at least 1 SuppressFinalize interleaved between ReRegisterForFinalize calls.Criticism
Thanks @Eric, I was aware that the code is in violation of the spec and I most certainly would not advocate using any of the above in a sensible program.Duro
Hey, masochists are programmers, too. Don't tread on the minorities, bro.Overwhelming
H
10

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.

Healion answered 4/11, 2011 at 14:55 Comment(2)
Nice call, Jon. Looking at the CLI source that MS released a while back, there's an if statement in the internal ReRegisterForFinalize() method that says "if I've been marked as finalized, remove that mark. If not, just add me to the queue." So if you call ReReg twice without supressing first, the object just gets added to the queue twice. Then, suppress clears the bit three times (which does nothing, really, since the bit was never set to begin with!) In any case, brilliant speculation, as always.Susurrous
Thanks @Jon and dlev, this is what I suspected. It is however contrary to my previous assumed (and provably incorrect) mental model, which was that one method adds and another removes from the finalization table. It's obviously against the documented usage, but seems an oversight that if an object is re-registered for finalisation that shouldn't be it can never be suppressed, I was curious what the reason was - I assume performance reasons. sehe's investigations into how mono behaves is quite interesting, I suppose that's the beauty of undefined behavior in the spec!Duro
L
5

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");
    }
}
Lumen answered 4/11, 2011 at 14:47 Comment(0)
H
2

I suspect this comes under the realm of 'undefined behaviour'. If you have a look at the documentation for ReRegisterForFinalize and SuppressFinalize, they say:

The obj parameter must be the caller of this method.

And that's not the case for your code.

Hugmetight answered 4/11, 2011 at 14:53 Comment(1)
Nice pickup, although interestingly enough it still occurs on .Net if you don't interleave at least 1 SuppressFinalize between ReRegisterForFinalize calls.Criticism

© 2022 - 2024 — McMap. All rights reserved.