Last time, I talked about the basics of using WeakReference, what they meant and how the CLR treats them. Today in part 2, I’ll discuss some important subtleties. Part 3 of this series can be found here.
Short vs. Long Weak References
First, there are two types of weak references in the CLR:
- Short – Once the object is reclaimed by garbage collection, the reference is set to null. All of the examples in the previous article, with WeakReference and WeakReference<T>, were examples of short weak references.
- Long – If the object has a finalizer AND the reference is created with the correct options, then the reference will point to the object until the finalizer completes.
Short weak references are fairly easy to understand. Once the garbage collection happens and the object has been collected, the reference gets set to null, the end. A short weak reference can only be in one of two states: alive or collected.
Using long weak references is more complicated because the object can be in one of three states:
- Object is still fully alive (has not been promoted or garbage collected).
- Object has been promoted and the finalizer has been queued to run, but has not yet run.
- The object has been cleaned up fully and collected.
With long weak references, you can retrieve a reference to the object during stages 1 and 2. Stage 1 is the same as with short weak references, but stage 2 is tricky. Now the object is in a possibly undefined state. Garbage collection has started, and as soon as the finalizer thread starts running pending finalizers, the object will be cleaned up. This can happen at any time, so using the object is very tricky. The weak reference to the target object remains non-null until the target object’s finalizer completes.
To create a long weak reference, use this constructor:
WeakReference<MyObject> myRefWeakLong = new WeakReference<MyObject>(new MyObject(), true);
The true argument specifies that you want to track resurrection. That’s a new term and it is the whole point of long weak references.
Aside: Resurrection
First, let me say this up front: Don’t do this. You don’t need it. Don’t try it. You’ll see why. I don’t know if there is a special reason why resurrection is allowed in .NET, or it’s just a natural consequence of how garbage collection works, but there is no good reason to do something like this.
So here’s what not to do:
class Program { class MyObject { ~MyObject() { myObj = this; } } static MyObject myObj = new MyObject(); static void Main(string[] args) { myObj = null; GC.Collect(); GC.WaitForPendingFinalizers(); } }
By setting the myObj reference back to an object, you are resurrecting that object. This is bad for a number of reasons:
- You can only resurrect an object once. Because the object has already been promoted to gen 1 by the garbage collector, it has a guaranteed limited lifetime.
- The finalizer will not run again, unless you call GC.ReRegisterForFinalize() on the object.
- The state of the object can be indeterminate. Objects with native resources will have released those resources and they will need to be reinitialized. It can be tricky picking this apart.
- Any objects that the resurrected object refers to will also be resurrected. If those objects have finalizers they will also have run, leaving you in a questionable state.
So why is this even possible? Some languages consider this a bug, and you should to. Some people use this technique for object pooling, but this is a particularly complex way of doing it, and there are many better ways. You should probably consider object resurrection a bug as well. If you do happen upon a legitimate use case for this, you should be able to fully justify it enough to override all of the objections here.
Weak vs. Strong vs. Finalizer Behavior
There are two dimensions for specifying a WeakReference<T>: the weak reference’s creation parameters and whether the object has a finalizer. The WeakReference’s behavior based on these is described in this table:
No finalizer | Has finalizer | |
trackResurrection = false | short | short |
trackResurrection = true | short | long |
An interesting case that isn’t explicitly specified in the documentation is when trackResurrection is false, but the object does have a finalizer. When does the WeakReference get set to null? Well, it follows the rules for short weak references and is set to null when the garbage collection happens. Yes, the object does get promoted to gen 1 and the finalizer gets put into the queue. The finalizer can even resurrect the object if it wants, but the point is that the WeakReference isn’t tracking it–because that’s what you said when you created it. WeakReference’s creation parameters do not affect how the garbage collector treats the target object, only what happens to the WeakReference.
You can see this in practice with the following code:
class MyObjectWithFinalizer { ~MyObjectWithFinalizer() { var target = myRefLong.Target as MyObjectWithFinalizer; Console.WriteLine("In finalizer. target == {0}", target == null ? "null" : "non-null"); Console.WriteLine("~MyObjectWithFinalizer"); } } static WeakReference myRefLong = new WeakReference(new MyObjectWithFinalizer(), true); static void Main(string[] args) { GC.Collect(); MyObjectWithFinalizer myObj2 = myRefLong.Target as MyObjectWithFinalizer; Console.WriteLine("myObj2 == {0}", myObj2 == null ? "null" : "non-null"); GC.Collect(); GC.WaitForPendingFinalizers(); myObj2 = myRefLong.Target as MyObjectWithFinalizer; Console.WriteLine("myObj2 == {0}", myObj2 == null ? "null" : "non-null"); }
The output is:
myObj2 == non-null In finalizer. target == non-null ~MyObjectWithFinalizer myObj2 == null
Finding Weak References in a Debugger
Windbg can show you how to find where your weak references, both short and long.
Here is some sample code to show you what’s going on:
using System; using System.Diagnostics; namespace WeakReferenceTest { class Program { class MyObject { ~MyObject() { } } static void Main(string[] args) { var strongRef = new MyObject(); WeakReference<MyObject> weakRef = new WeakReference<MyObject>(strongRef, trackResurrection: false); strongRef = null; Debugger.Break(); GC.Collect(); MyObject retrievedRef; // Following exists to prevent the weak references themselves // from being collected before the debugger breaks if (weakRef.TryGetTarget(out retrievedRef)) { Console.WriteLine(retrievedRef); } } } }
Compile this program in Release mode.
In Windbg, do the following:
- Ctrl+E to execute. Browse to the compiled program and open it.
- Run command: sxe ld clrjit (this tells the debugger to break when the clrjit.dll file is loaded, which you need before you can execute .loadby)
- Run command: g
- Run command .loadby sos clr
- Run command: g
- The program should now break at the Debugger.Break() method.
- Run command !gchandles
You should output similar to this:
0:000> !gchandles
Handle Type Object Size Data Type
011112f4 WeakShort 02d324b4 12 WeakReferenceTest.Program+MyObject
011111d4 Strong 02d31d70 36 System.Security.PermissionSet
011111d8 Strong 02d31238 28 System.SharedStatics
011111dc Strong 02d311c8 84 System.Threading.ThreadAbortException
011111e0 Strong 02d31174 84 System.Threading.ThreadAbortException
011111e4 Strong 02d31120 84 System.ExecutionEngineException
011111e8 Strong 02d310cc 84 System.StackOverflowException
011111ec Strong 02d31078 84 System.OutOfMemoryException
011111f0 Strong 02d31024 84 System.Exception
011111fc Strong 02d3142c 112 System.AppDomain
011113ec Pinned 03d333a8 8176 System.Object[]
011113f0 Pinned 03d32398 4096 System.Object[]
011113f4 Pinned 03d32178 528 System.Object[]
011113f8 Pinned 02d3121c 12 System.Object
011113fc Pinned 03d31020 4424 System.Object[]
Statistics:
MT Count TotalSize Class Name
70e72554 1 12 System.Object
01143814 1 12 WeakReferenceTest.Program+MyObject
70e725a8 1 28 System.SharedStatics
70e72f0c 1 36 System.Security.PermissionSet
70e724d8 1 84 System.ExecutionEngineException
70e72494 1 84 System.StackOverflowException
70e72450 1 84 System.OutOfMemoryException
70e722fc 1 84 System.Exception
70e72624 1 112 System.AppDomain
70e7251c 2 168 System.Threading.ThreadAbortException
70e35738 4 17224 System.Object[]
Total 15 objects
Handles:
Strong Handles: 9
Pinned Handles: 5
Weak Short Handles: 1
The weak short reference is called a “Weak Short Handle” in this output.
Next Time
The first article explained how WeakReference works, and this one explained a few of the subtleties, including some behavior you probably don’t want to use. Next time, I’ll go into why you would want to use WeakReference in the first place, and provide a sample application.