Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

GC of delegates, what am I missing? (my delegate is not collected)

I have a class that holds on to a delegate, in order to lazily evaluate something later.

Once I've evaluated it, by calling the delegate, I clear out the reference to the delegate, hoping that it would be eligible for collection. After all, it might hold on to a world of local variables if it was constructed as an anonymous method.

I tried building a unit-test to verify this, but it doesn't seem to work out the way I planned, instead it seems that either my assumptions about WeakReference (which I used for test purposes here), or some other assumption, doesn't hold water.

Take a look at this code, which you can run in LINQPad

void Main()
{
    WeakReference wr;
    Lazy<int> l;
    CreateTestData(out wr, out l);

    wr.IsAlive.Dump();                  // should be alive here

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

    wr.IsAlive.Dump();                  // and alive here as well
    l.Value.Dump();                     // but now we clear the reference

    GC.Collect();                       // so one of these should collect it
    GC.WaitForPendingFinalizers();
    GC.Collect();

    wr.IsAlive.Dump();                  // and then it should be gone here
    GC.KeepAlive(l);
}

void CreateTestData(out WeakReference wr, out Lazy<int> l)
{
    Func<int> f = () => 10;
    wr = new WeakReference(f);
    l = new Lazy<int>(f);
}

public class Lazy<T>
{
    private Func<T> _GetValue;
    private T _Value;

    public Lazy(Func<T> getValue)
    {
        _GetValue = getValue;
    }

    public T Value
    {
        get
        {
            if (_GetValue != null)
            {
                _Value = _GetValue();
                _GetValue = null;
            }
            return _Value;
        }
    }
}

I was assuming that:

  1. Regardless of DEBUG build, debugger attached, since I created the delegate in a separate method, which I return from, there should be nothing holding on to the delegate, except the WeakReference and the Lazy<T> objects
  2. If I ask the Lazy<T> object to relinquish its reference to the delegate, which would reduce the references to only the one that WeakReference is holding on to
  3. And then force a full garbage collection, assuming that if the only reference left is the one in WeakReference
  4. Then the delegate would be collected, and my WeakReference would indicate that the object is no longer alive

The output of the code was thus expected to be (with comments):

true  // not gc'ed after construction
true  // not gc'ed after full GC, still beind held by Lazy<T>
10    // value from calling delegate
false // but is now gc'ed, Lazy<T> no longer has a reference to it

But instead the output is:

true
true
10
true

Can anyone shed some light on what I'm missing here?

like image 871
Lasse V. Karlsen Avatar asked Nov 30 '11 12:11

Lasse V. Karlsen


1 Answers

The "problem" is that the compiler is noticing that it can reuse a single delegate instance forever. It doesn't capture any context, not even the implicit this reference. So this:

void CreateTestData(out WeakReference wr, out Lazy<int> l)
{
    Func<int> f = () => 10;
    ...
}

Is turned into something like:

static Func<int> hiddenDelegate;

static int HiddenMethod()
{
    return 10;
}

void CreateTestData(out WeakReference wr, out Lazy<int> l)
{
    if (hiddenDelegate == null)
    {
        hiddenDelegate = HiddenMethod;
    }

    Func<int> f = hiddenDelegate;
    ...
}

Look at the code in ildasm (or Reflector with no optimization turned on) to see exactly what's going on.

like image 105
Jon Skeet Avatar answered Oct 17 '22 00:10

Jon Skeet