次の方法で共有


The Truth About GCHandles

I've heard several people asking why GCHandle doesn't implement IDisposable, considering it wraps an unmanaged resource (a handle) and needs to be explicitly freed (using GCHandle.Free()). Before I explain the reason, I want to give a little background on GCHandles and their dangers.

What's a GCHandle?

A GCHandle is a struct that contains a handle to an object. It's mainly used for holding onto a managed object that gets passed to the unmanaged world to prevent the GC from collecting the object. You can also create a Pinned GCHandle to a managed object and retrieve the object's address in memory.

How are GCHandles dangerous?

When you create a new GCHandle, a new entry in the AppDomain's handle table is created. This entry is kept until the handle is freed (via GCHandle.Free()) or the AppDomain is unloaded.

Things get tricky if you were to make a copy of a GCHandle:

Object obj = new Object();
GCHandle gch = GCHandle.Alloc(obj, GCHandleType.Normal);
GCHandle gch2 = gch;

Since GCHandle is value type, gch2 has its own copy of the handle. You now have two handles that point to the same entry in the handle table. Unfortunately, since gch2 is a copy of –not a reference to– gch, anything that happens to gch doesn't happen to gch2. For example, calling gch.Free() will delete the entry from the handle table, but not update gch2, so gch2.IsAllocated will return true, but gch2.Target will be null. The same problem arises with casting to and from IntPtrs, and when GCHandles get boxed. Unlike double-freeing a single GCHandle, freeing the copy will NOT throw an InvalidOperationException. You have to be very careful not to double-Free your handles since this can corrupt the handle table.

Why don't GCHandles implement IDisposable?

One of the main purposes of IDisposable to avoid the use of finalizers. This is because finalizers are not run deterministically, and result in promoting a finalizable object a generation, effectively keeping in memory longer. Since GCHandle is a value type, it has no finalizer, and is not collected by the GC, so these problems are eliminated. Another other main use of IDisposable is to clean up unmanaged resources as soon as you are done with them. With a GCHandle, the resource is the handle which is cleaned up by calling GCHandle.Free(). If Free isn't called, the handle gets cleaned up when the appdomain is unloaded.

One of the side effects of having a struct implement IDisposable, is that users may be tempted to cast their GCHandles as IDisposable, which boxes the GCHandle into an IDisposable object on the heap, and the two GCHandles get out-of-sync. The same problem arises with the putting a disposable struct into a using block:

struct Test : IDisposable
{
    public bool disposed; // initialized to false
    public void Dispose()
    {
        disposed = true;
    }
}

public void Foo()
{
    Test t = new Test();
    using (t)
    {
        // do stuff
    }

    if (!t.disposed)
    {
        t.Dispose();
    }
}

t.disposed will return false, since it was a copy of t whose Dispose method was called. If t were a GCHandle, then the handle would be removed from the appdomain's handle table, and calling Free after the using would result in a double Free, even though IsAllocated would return true!

Remember, GCHandles are advanced structures, and one should be very careful to ensure they are cleaned up properly. Unfortunately, IDisposable makes it easy to get this wrong, so the BCL designers erred on the side of caution, and gave GCHandle a Free() method to use.

Comments

  • Anonymous
    March 18, 2005
    The prosecution rests, Your Honor.

    I now know why I was wrong to say that GCHandles should implement IDisposable. Thx for the thorough explanation.
  • Anonymous
    March 22, 2005
    Excellent article!
  • Anonymous
    February 01, 2006
    The comment has been removed
  • Anonymous
    February 02, 2006
    Clint, if you're manipulating the handle via the IntPtr, you have to be very careful that you're not double freeing. Checking both IsAllocated and Target is probably a good idea.
  • Anonymous
    March 28, 2006
    Hi,

    In my application, the GC handles reduce with time and show impossible values. I mean very wierd negative, large values.

    I run a web service.

    Any clues on what this may be?

    Thanks in advance for ur help! Any clue is highly appreciated.

    Meenakshi.

  • Anonymous
    March 29, 2006
    Hi Meenakshi

    You'll have to provide more information.  How are you measuring the number of GCHandles?  

    I recommend you post your question on the .NET forums so others can help you out.
    http://forums.microsoft.com/MSDN/ShowForum.aspx?ForumID=39&SiteID=1

    -Chris