10

Consider the following code:

class Program
{
    static void Main(string[] args)
    {
        A a = new A();
        CreateB(a);

        GC.Collect();
        GC.WaitForPendingFinalizers();
        Console.WriteLine("And here's:" + a);
        GC.KeepAlive(a);
    }

    private static void CreateB(A a)
    {
        B b = new B(a);
    }
}

class A
{ }

class B
{
    private WeakReference a;
    public B(A a)
    {
        this.a = new WeakReference(a);
    }

    ~B()
    {
        Console.WriteLine("a.IsAlive: " + a.IsAlive);
        Console.WriteLine("a.Target: " + a.Target);
    }
}

With the following output:

a.IsAlive: False
a.Target:
And here's:ConsoleApp.A

Why is it false and null? A hasn't been collected yet.

EDIT: Oh ye of little faith.

I added the following lines:

Console.WriteLine("And here's:" + a);
GC.KeepAlive(a);

See the updated output.

MatthewMartin
  • 32,326
  • 33
  • 105
  • 164
Bubblewrap
  • 7,266
  • 1
  • 35
  • 33

6 Answers6

3

Updated answer for updated question.

With the new question, we go through the following steps.

  1. A and B alive and rooted, B.a alive and rooted via B.
  2. A alive, B not rooted and eligible for collection. B.a not rooted and eligible.
  3. Collection happens. B and B.a are each finalisable, so they are put on the finaliser queue. B is not collected because it is finalisable. B.a is not collected, both because it is finalisable, and because it is referenced by B which has not yet been finalised.
  4. Either B.a is finalised, or B is finalised.
  5. The other of B.a or B is finalised.
  6. B.a and B are eligibled for for collection.

(If B was finalised at point 4 it would be possible for it to be collected before point 5, as while B awaiting finalisation keeps both B and B.a from collection, B.a awaiting finalisation does not affect B's collection).

What has happened is that the order between 4 and 5 was such that B.a was finalised and then B was finalised. Since the reference a WeakReference holds to an object is not a normal reference, it needs its own clean-up code to release its GCHandle. Obviously it can't depend on normal GC collection behaviour, since the whole point of its references is that they don't follow normal GC collection behaviour.

Now B's finaliser is run, but since the behaviour of B.a's finaliser was to release its reference it returns false for IsAlive (or in .NET prior to 1.1 if I'm remembering the versions right, throws an error).

Jon Hanna
  • 110,372
  • 10
  • 146
  • 251
  • @Brian. It's now a different question, so I've given a different answer. – Jon Hanna Aug 18 '10 at 09:11
  • @Jon: So I figured. Just wanted to let you know. – Brian Rasmussen Aug 18 '10 at 10:01
  • Marked as answer, as it is more accurate than Marc's post. I never even realized there was a difference between finalising and collection, which is the real cause of the behaviour i'm seeing. – Bubblewrap Aug 18 '10 at 11:11
  • @Bubblewrap. It's a very important difference. Finalisation is to deal with stuff that collection won't and Dispose hasn't (if Dispose does all clean-up it should call SupressFinalize so the finaliser will only be called if Dispose wasn't called already). Finalisation actually gets in the way of collection, to make sure it gets its chance to happen, hence the importance of suppressing when you can and not writing finalisers you don't need. Collection is *just* cleaning up managed memory, Dispose is cleaning everything else explicitly, Finalisers clean up what's left before collection. – Jon Hanna Aug 18 '10 at 11:21
  • But then, only in theory, GC.SuppressFinalize could've been of use here? It's just that we can't use it on the WeakReference because the parameter has to be the caller of this method? – Bubblewrap Aug 18 '10 at 11:29
  • After GC.SuppressFinalize(this) the finalizer will never be called. You do it if you have already (most likely in Dispose() though it could be elsewhere) put the object in a state where you know the finaliser will never be needed. Finalisers are a big strain on letting the GC clean up memory, so suppressing them when you can is a good idea. – Jon Hanna Aug 18 '10 at 11:36
2

The key problem in this is that you are accessing a reference field during a finalizer. The underlying problem is that the WeakReference itself has (or can be, unpredictably) already been collected (since collection order is non-deterministic). Simply: the WeakReference no longer exists, and you are query IsValid / Target etc on a ghost object.

So accessing this object at all is unreliable and brittle. Finalizers should only talk to direct value-type state - handles, etc. Any reference (unless you know it will always out-live the object being destroyed) should be treated with distrust and avoided.

If instead, we pass in the WeakReference and ensure that the WeakReference is not collected, then everything works fine; the following should show one success (the one where we've passed in the WeakReference), and one fail (where we've created the WeakReference just for this object, thus it is eligible for collection at the same time as the object):

using System;
class Program
{
    static void Main(string[] args)
    {
        A a = new A();
        CreateB(a);

        WeakReference weakRef = new WeakReference(a);
        CreateB(weakRef);
        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.KeepAlive(a);
        GC.KeepAlive(weakRef);

        Console.ReadKey();
    }

    private static void CreateB(A a)
    {
        B b = new B(a);
    }
    private static void CreateB(WeakReference a)
    {
        B b = new B(a);
    }
}

class A
{ }

class B
{
    private WeakReference a;
    public B(WeakReference a)
    {
        this.a = a;
    }
    public B(A a)
    {
        this.a = new WeakReference(a);
    }

    ~B()
    {
        Console.WriteLine("a.IsAlive: " + a.IsAlive);
        Console.WriteLine("a.Target: " + a.Target);
    }
}

What makes you say it isn't collected? It looks eligible.... no field on a live object holds it, and the variable is never read past that point (and indeed that variable may well have been optimised away by the compiler, so no "local" in the IL).

You might need a GC.KeepAlive(a) at the bottom of Main to stop it.

Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
  • @Bubblewrap - indeed, I had this discussion in the reply Luther's reply. In theory it *should*. I'm... unclear why. – Marc Gravell Aug 18 '10 at 08:09
  • @Bubblewrap - Revised; should make sense now. – Marc Gravell Aug 18 '10 at 08:24
  • Thanks, i suspected it was something like this, but couldn't much info about it. Would it also be possible to somehow use GC.SuppressFinalize to be able to use A in ~B? – Bubblewrap Aug 18 '10 at 08:35
  • @Bubblewrap - that just prevents the finalizer (if one) from being invoked. It doesn't stop it getting collected. So no: not really. Simply - WeakReference *in a finalizer* is not going to be happy (unless the WeakReference is also referenced somewhere external to the object) – Marc Gravell Aug 18 '10 at 08:47
  • 1
    It's **not** true that you can't access a reference field from a finaliser, as the entire graph will be placed on the finaliser queue. Were B the only finalisable object here it would become a finaliser queue root blocking the collection of both B and B.a. It's because B.a is also finalisable that it is also a finaliser queue root. B.a and B will have finalisers run (no order promised) and only after both of them are out of the finaliser queue is either eligible for collection. Hence you can safely use a non-finalisable reference, or methods not affected by the finaliser. – Jon Hanna Aug 18 '10 at 09:16
2

This is indeed a bit odd and I can't say, that I have the answer, but here's what I have found so far. Given your example, I attached WinDbg immediately before the call to GC.Collect. At this point the weak reference holds on to the instance as expected.

Following that I dug out the actual instance of WeakReference and set a data breakpoint on the reference itself. Proceeding from this point the debugger breaks at mscorwks!WKS::FreeWeakHandle+0x12 (which sets the handle to null) and the managed call stack is as follows:

OS Thread Id: 0xf54 (0)
ESP       EIP     
0045ed28 6eb182d3 [HelperMethodFrame: 0045ed28] System.GC.nativeCollectGeneration(Int32, Int32)
0045ed80 00af0c62 System.GC.Collect()
0045ed84 005e819d app.Program.Main(System.String[])
0045efac 6eab1b5c [GCFrame: 0045efac] 

This seems to indicate, that the call to GC.Collect in turn ends up modifying the weak reference as well. This would explain the observed behavior, but I can't say if this is how it will behave in all cases.

Brian Rasmussen
  • 114,645
  • 34
  • 221
  • 317
  • Look a level higher in abstraction. The WeakReference has a destructor of its own, which **may** be called before B's destructor. Prior to .NET2 the code above would have thrown an exception in this case, but it was changed to just have WeakReference act like an "empty" WeakReference (which it is, really). – Jon Hanna Aug 18 '10 at 09:30
  • @Jon: Thanks for the update. To be honest, I didn't pursue it any further, as I found Marc's answer to be sufficient. – Brian Rasmussen Aug 18 '10 at 10:03
  • Marc's answer is wrong as he says the problem is that a reference field is accessed in a finaliser. Accessing a reference field in a finaliser is perfectly safe in itself. It's if that reference field is to an object that itself has a finaliser that it may be in a finalised state. Still not an issue to access it *per se*, but results may be unexpected (and even disasterous, the above would error in .NET1.1). But not only can you access reference fields, but you very often need to. – Jon Hanna Aug 18 '10 at 10:16
  • @Jon: Okay, perhaps this does warrant a bit more investigation. Thanks for the update. – Brian Rasmussen Aug 18 '10 at 10:19
1

Why is it false and null? A hasn't been collected yet.

You don't know that for sure. The GC can collect it as soon as it's no longer needed - which, in this case, is right after it gets stuffed into the WeakReference.

Incidentally, Raymond Chen had a blog post recently about this very topic.

Anon.
  • 58,739
  • 8
  • 81
  • 86
  • Right after it gets stuffed in the WeakReference? Are you serious? Then the whole WeakReference bit is useless, right at that point, isn't it? – Arcturus Aug 18 '10 at 08:01
  • @Arcturus: The whole point of WeakReferences is that they don't prevent collection. If you're stuffing something into a weak reference, and then not using any strong references at all after that, you should not be surprised if it gets collected out from under you. The contract of a WeakReference is quite explicit about the fact that it's not going to prevent the object being collected. – Anon. Aug 18 '10 at 08:09
  • Thats all nice and all, but the stuffing happens in the constructor. I find it hard to believe, A gets collected right there, in the constructor of B. – Arcturus Aug 18 '10 at 08:13
  • @Arcturus: Read the blog post I linked. It explains this in much more detail than I am capable of doing in these comment boxes. – Anon. Aug 18 '10 at 08:15
  • I've read the blog post and they talk specifically about methods. I wonder if it would be the same for constructors. – Arcturus Aug 18 '10 at 08:25
1

The garbage collector has determined that ais dead because it is not referred anymore after GC.collect(). If you change the code to:

GC.Collect();
GC.WaitForPendingFinalizers();
System.Console.WriteLine("And here's:"+a);

You will find a alive during the finalization of B.

Nordic Mainframe
  • 28,058
  • 10
  • 66
  • 83
0

Even though WeakReference does not implement IDisposable, it does use an unmanaged resource (a GCHandle). When a WeakReference is abandoned, it must make certain that resource gets released before the WeakReference itself gets garbage-collected; if it did not, the system would have no way of knowing that the GCHandle was no longer needed. To deal with this, a WeakReference release its GCHandle (thus invalidating itself) in its Finalize method. If this happens before the execution of a Finalize method that attempts to use the WeakReference, the latter method will have no way of getting the WeakReference's former target.

The constructor for WeakReference accepts a parameter which indicates whether its target should be invalidated as soon as its target becomes eligible for immediate finalization (parameter value false), or only when its target becomes eligible for annihilation (parameter value true). I'm not sure whether that parameter will cause the WeakReference itself to be resurrected for one GC cycle, but that might be a possibility.

Otherwise, if you are using .net 4.0, there is a class called ConditionalWeakTable that might be helpful; it allows the lifetimes of various objects to be linked. You could have a finalizable object which holds a strong reference to the object to which you want a weak reference, and have the only reference to that finalizable object be stored in a ConditionalWeakTable, keyed by the object to which the weak reference is desired. The latter object (the Value of the ConditionalWeakTable entry) will become eligible for finalization when its correspondingKey` does; it could then do something suitable with the strong reference it holds.

supercat
  • 77,689
  • 9
  • 166
  • 211