Why garbage collector takes objects in the wrong o

2019-01-28 03:31发布

问题:

I have an application with two classes, A and B. The class A has inside a reference to class B. The destructors of the classes do some cleanup of resources but they have to be called in the right order, first the destructor of A and then the destructor of B.

What is happening is that somehow the destructor of B is called first and then the destructor of A is crashing because is trying to execute methods from a disposed object.

Is this behavior of the GC correct? I expected the GC to detect that A has a reference to B and then call the A destructor first. Am I right?

Thanks mates!

PD: In case of doubt about destructor/finalizer/disposer etc that's what we have:

~A()
{
    this.Dispose();
}

~B()
{
    this.Dispose();
}    

回答1:

The GC is nondeterministic: it is not guaranteed to garbage-collect your objects in any particular order, or at any time, or even ever---it could decide to keep them around after your program is completed, if it wanted to. Thus finalizers are very rarely useful or used.

If you want deterministic finalization, you should use the IDisposable pattern.



回答2:

As others have noted, your finalizers are wrong, wrong, wrong. You cannot simply call Dispose in a finalizer and expect good things to happen. Read up on the correct way to implement the disposable pattern.

Getting that right is the beginning, not the end, of the work you have to do. In addition to all the other correct answers here, I note that:

  • the finalizer can (and usually does) run on a different thread. Finalizing resources that have an affinity to a particular thread is dangerous, you can run into deadlocks if you are not careful, and so on.

  • finalization can "resurrect" a dead object by assigning a reference to it to a variable that is in a live object. Do not do that. It is incredibly confusing.

  • finalizers can run on objects that were partially constructed when a thread abort exception happened. You cannot assume that any invariant that is true of a fully-constructed object is true of an object being finalized.

Writing a finalizer correctly is extremely difficult for all these reasons. Avoid, avoid, avoid.



回答3:

From http://msdn.microsoft.com/en-us/magazine/bb985010.aspx:

The runtime doesn't make any guarantees as to the order in which Finalize methods are called. For example, let's say there is an object that contains a pointer to an inner object. The garbage collector has detected that both objects are garbage. Furthermore, say that the inner object's Finalize method gets called first. Now, the outer object's Finalize method is allowed to access the inner object and call methods on it, but the inner object has been finalized and the results may be unpredictable. For this reason, it is strongly recommended that Finalize methods not access any inner, member objects.



回答4:

You should not write code that depends on the deconstructors in C#, they might never be run so can not be depended upon.



回答5:

The correct way to write finalizers is to not call anything on related objects, you should only destroy unmanaged resources through the finalizer, precisely because, as you've noted, things happen out of order.

As others have noted, this is documented as part of the specification, so this is not a bug, nor is it something that will change to "do the right thing". In particular, think about two objects both having a reference to the other, it would be impossible for the runtime to figure out what the correct order would be here.

In any case, here's how you should implement a finalizer. Note that you should only implement a finalizer if you need to destroy unmanaged resources (note, there are some edge-cases where you might want a finalizer for a very special-purpose class, but I won't mention these here any more than this):

public class Test : IDisposable
{
    ~Test()
    {
        Dispose(false);
    }

    public void Dispose()
    {
        Dispose(true);
    }

    protected virtual void Dispose(bool disposing)
    {
        // TODO: Make sure calling Dispose twice is safe
        if (disposing)
        {
            // call Dispose method on other objects
            GC.SuppressFinalize(this);
        }

        // destroy unmanaged resources here
    }
}

You can find more information here: Implementing a Dispose Method.