C#弱引用(WeakReference)

弱引用基础概念

弱引用:持有对象的引用,但允许垃圾回收销毁对象并回收内存。
强引用:持有对象的引用,防止GC回收引用对象

弱引用 的两个版本:

  • WeakReference

  • WeakReference<T>.

第一个版本从 .NET 1.1. 就已经存在,可以用以下代码实例化WeakReference

var weakRef = new WeakReference(myObj);myObj = null;
 

myObj 是一个已经实例化的对象。一旦将其分配给weakRef变量,就应该将原始强引用设置为null。现在,只要有垃圾回收,就可以回收weakRef所指的对象。

你可能想通过WeakReference的IsAlive属性,判断是否可以访问弱引用的对象,如下例所示:

WeakReference ref1 = new WeakReference(new MyObject());if (ref1.IsAlive){    // wrong!!!!!    DoSomething(ref1.Target as MyObject);}

IsAlive 这个属性并没有什么软用,当值为false时,表示对象被回收了,不会有什么问题。但是当值为true时,有可能在对象添加强引用的一瞬前,对象就被垃圾回收机制回收掉。
正确的使用弱引用指定的对象,如下所示:

MyObject obj = ref1.Target as MyObject;if (obj != null){    // correct    DoSomething(obj);}

你可能会问,既然可以通过WeakReference<T>实例化一个弱引用对象,为什么还会存在SetTarget方法呢?

短弱引用 vs 长弱引用

在CLR(.NET运行时)中存在两种弱引用:

  • Short Weak Reference(短弱引用):一旦对象被垃圾回收,引用就会设置为null,上文所举的例子都是短弱引用。

  • Long Weak Reference(长弱引用):如果对象存在一个finalizer(终结器/析构函数) 并且引用被以正确的方式创建。那么引用将一直指向对象,直到finalizer结束。

短弱引用相对比较好理解,一旦发生垃圾回收,对象被回收销毁。引用就被设置为空。一个短弱引用只有两种状态:alive(存活) 或者 collected(被回收)

长弱引用相对复杂些,因为引用的对象可以处于以下三种状态之一:

  1. 对象仍处于活动状态(对象还没被回收,也没被标注要进行回收)

  2. 对象已经被标注要进行回收,finalizer已排队等待运行,但尚未运行。

  3. 物体已完全清理并收集。

对于长弱引用,你可以在状态1和状态2中获取到对象的引用。状态1与短弱引用相同,但状态2很棘手。该状态下对象处可能未定义。垃圾收集已经开始,一旦终结器线程开始运行挂起的finalizer,该对象将被清除。这可能在任何时候发生,因此使用该对象非常棘手。在目标对象的finalizer完成之前,对目标对象的弱引用保持非null。

可以使用以下构造函数,创建长弱引用:

WeakReference<MyObject> myRefWeakLong     = new WeakReference<MyObject>(new MyObject(), true);

参数true表示要 track resurrection(跟踪恢复),这是新术语,也是长弱引用的重点。

题外话:Resurrection(复活)

首先,强调下:不要这样做。你不需要它。不要试试。你会明白为什么。我不知道在.NET中是否允许Resurrection(复活)是有特殊原因的,或者它只是垃圾回收工作的自然结果,但是没有充分的理由去做这样的事情。

所以这是不该做的:

class Program{    class MyObject    {        ~MyObject()        {            myObj = this;        }    }    static MyObject myObj = new MyObject();    static void Main(string[] args)    {        myObj = null;        GC.Collect();        GC.WaitForPendingFinalizers();    }}

通过将myObj的引用设置给一个对象,而复活myObj。这样处理有非常多坏处:

  • 只能复活一个对象一次,因为该对象已经被垃圾回收机制标记为gen 1。所以这个对象的生命周期有限制。

  • 除非你在这个对象上调用GC.ReRegisterForFinalize() ,否则finalizer(终结器)不会再次运行。

  • 对象处于不可确定状态,对象加载的本地资源已经被释放,需要被重新实例化。单独处理这些相对麻烦。

  • 复活对象引用的对象也会被复活。如果这些对象存在finalizers(终结器),这些终结器也有可能已经被执行。这些对象的状态也不好确认。

那为什么这种情况还会发生?有些语言认为这是个bug,你也会这么认为。有些人把Resurrection(复活)这技术用于对象池,但这是比较复杂的做法,而且存在很多更好的方式去实现。你应该将对象复活视为一个bug。

弱引用 vs 强引用 vs 终结器行为

WeakReference<T>可以用两个维度来标识:弱引用初始化时的参数,以及该弱引用是否有finalizer。

 No finalizerHas finalizer
trackResurrection = falseshortshort
trackResurrection = trueshortlong

一个有趣的案例没有在文档中明确指出,当trackResurrection为false时,该对象确实有一个终结器。弱引用什么时候被设置为null?它遵循short weak references(短弱引用)的规则,垃圾回收时设置为null。那么,垃圾回收时对象会被标识为gen 1,终结器也被添加到执行队列。这时候终结器仍然可以复活对象。不过重点是,弱引用trackResurrection为false(没有监听复活)。弱引用的参数不会影响到垃圾回收机制,只是影响到弱引用本身。
你可以使用以下代码在实践中看到这一点:

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"); }

输出结果如下

myObj2 == non-null In finalizer. target == non-null ~MyObjectWithFinalizer myObj2 == null
 
调试器中查找弱引用

Windbg可以向您展示如何找到您的弱引用的位置,包括短弱引用和长弱引用。

以下是一些示例代码:

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);             }         }     } }

Release 模式编译代码

In Windbg, 使用一下操作

  1. Ctrl+E to execute. Browse to the compiled program and open it.

  2. 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)

  3. Run command: g

  4. Run command .loadby sos clr

  5. Run command: g

  6. The program should now break at the Debugger.Break() method.

  7. Run command !gchandles

你可以得到类似下面的输出:

0:000> !gchandles  Handle Type          Object     Size     Data Type011112f4 WeakShort   02d324b4       12          WeakReferenceTest.Program+MyObject011111d4 Strong      02d31d70       36          System.Security.PermissionSet011111d8 Strong      02d31238       28          System.SharedStatics011111dc Strong      02d311c8       84          System.Threading.ThreadAbortException011111e0 Strong      02d31174       84          System.Threading.ThreadAbortException011111e4 Strong      02d31120       84          System.ExecutionEngineException011111e8 Strong      02d310cc       84          System.StackOverflowException011111ec Strong      02d31078       84          System.OutOfMemoryException011111f0 Strong      02d31024       84          System.Exception011111fc Strong      02d3142c      112          System.AppDomain011113ec Pinned      03d333a8     8176          System.Object[]011113f0 Pinned      03d32398     4096          System.Object[]011113f4 Pinned      03d32178      528          System.Object[]011113f8 Pinned      02d3121c       12          System.Object011113fc Pinned      03d31020     4424          System.Object[]Statistics:      MT    Count    TotalSize Class Name70e72554        1           12 System.Object01143814        1           12 WeakReferenceTest.Program+MyObject70e725a8        1           28 System.SharedStatics70e72f0c        1           36 System.Security.PermissionSet70e724d8        1           84 System.ExecutionEngineException70e72494        1           84 System.StackOverflowException70e72450        1           84 System.OutOfMemoryException70e722fc        1           84 System.Exception70e72624        1          112 System.AppDomain70e7251c        2          168 System.Threading.ThreadAbortException70e35738        4        17224 System.Object[]Total 15 objectsHandles:    Strong Handles:       9    Pinned Handles:       5    Weak Short Handles:   1

短弱引用被标识为“Weak Short Handle”

WeakReference的实际用途

何时使用WeakReference

简单的说:很少。大多数应用程序不需要这个。
长一点的:如果满足以下所有条件,那么您可能需要考虑它:

  1. 内存需要严格限制,就目前而言,很可能是移动设备。如果是在Windows RT 或者 Android那么内存会被严格限制

  2. 对象的生命周期是高度可变的 - 如果你可以很好地预测对象的生命周期,那么使用WeakReference并没有多大意义。在这种情况下,你应该直接控制对象的生命周期。

  3. 对象相对较大,但易于创建 -  WeakReference非常适合那些内存占用很大的对象,但如果没有,你可以根据需要轻松地重新生成它(或者不实用弱引用)。

  4. 对象的大小远远大于使用WeakReference<T>的开销 - 使用WeakReference <T>添加了一个额外的对象,这意味着更多的内存压力,一个额外的解除引用步骤。使用WeakReference <T>来存储一个比WeakReference <T>本身差不多的对象将完全浪费时间和内存。