Advertisement

聊一聊 C# 弱引用 底层是怎么玩的

阅读量:

一:背景

1. 讲故事

最近在深入研究 dump 时, 发现程序出现 hang 情况与 WeakReference 有关, 在以往只知道如何操作该工具而对其内部运行机制却一无所知, 借此机会对这一问题进行初步探讨

二:弱引用的玩法

1. 一些基础概念

熟悉使用WeakReference的朋友都清楚,在这种情况下还可以将其划分为两个具体概念:弱短弱长;这些概念分别对应于构造函数中的trackResurrection参数,并且它们也是一种对底层GCHandle.Alloc方法的封装;参考源码如下:

复制代码
  
    
 public WeakReference(object? target, bool trackResurrection)
    
 {
    
     Create(target, trackResurrection);
    
 }
    
  
    
 private void Create(object target, bool trackResurrection)
    
 {
    
     nint num = GCHandle.InternalAlloc(target, trackResurrection ? GCHandleType.WeakTrackResurrection : GCHandleType.Weak);
    
     _taggedHandle = (trackResurrection ? (num | 1) : num);
    
     ComAwareWeakReference.ComInfo comInfo = ComAwareWeakReference.ComInfo.FromObject(target);
    
     if (comInfo != null)
    
     {
    
     ComAwareWeakReference.SetComInfoInConstructor(ref _taggedHandle, comInfo);
    
     }
    
 }
    
  
    
 public enum GCHandleType
    
 {
    
     //
    
     // Summary:
    
     //     This handle type is used to track an object, but allow it to be collected. When
    
     //     an object is collected, the contents of the System.Runtime.InteropServices.GCHandle
    
     //     are zeroed. Weak references are zeroed before the finalizer runs, so even if
    
     //     the finalizer resurrects the object, the Weak reference is still zeroed.
    
     Weak = 0,
    
     //
    
     // Summary:
    
     //     This handle type is similar to System.Runtime.InteropServices.GCHandleType.Weak,
    
     //     but the handle is not zeroed if the object is resurrected during finalization.
    
     WeakTrackResurrection = 1
    
 }
    
    
    
    
    
![](https://ad.itadn.com/c/weblog/blog-img/images/2025-07-12/QdJqFsbRYgM3aTSpjkwh0rPxDiLO.png)

从上面的 GCHandleType 的注释来看。

Weak会在终结器执行前对持有的对象进行检测,并判断其是否属于垃圾对象范畴;如果符合条件,则会立即切断引用关系。

WeakTrackResurrection 会在终结器执行完毕后识别目标对象是否为垃圾对象,并根据结果立即终止引用连接。

可能这么说有点抽象,画张图如下:

2. 一个简单的测试例子

为了方便讲述两者的区别,使用 对象复活 来做测试。

1、Weak 的情况

因为在这个ScanForFinalization方法的前一步进行了判断操作

复制代码
  
    
     class Program
    
     {
    
     static void Main()
    
     {
    
         WeakReferenceCase();
    
  
    
         GC.Collect();
    
         GC.WaitForPendingFinalizers();
    
  
    
         Console.WriteLine(weakHandle.Target ?? "Person 引用被切断");
    
  
    
         Console.ReadLine();
    
     }
    
  
    
     public static GCHandle weakHandle;
    
  
    
     static void WeakReferenceCase()
    
     {
    
         var person = new Person() { ressurect = false };
    
         weakHandle = GCHandle.Alloc(person, GCHandleType.Weak);
    
     }
    
     }
    
  
    
     public class Person
    
     {
    
     public bool ressurect = false;
    
  
    
     ~Person()
    
     {
    
         if (ressurect)
    
         {
    
             Console.WriteLine("Person 被永生了,不可能被消灭的。。。");
    
             GC.ReRegisterForFinalize(this);
    
         }
    
         else
    
         {
    
             Console.WriteLine("Person 析构已执行...");
    
         }
    
     }
    
     }
    
    
    
    
    
![](https://ad.itadn.com/c/weblog/blog-img/images/2025-07-12/sDg13te50OqZA6PkSVNvjrHxmXwh.png)

2、WeakTrackResurrection 的情况

在 ScanForFinalization 被执行后进行的判断中,在这种情况下可能会出现 对象复活 的现象。这样就将原本被认为是‘垃圾’的东西重新判定为重要物品,并通过以下方式处理:如果出现这种情况,则不应进行切断操作,请参考代码如下:

复制代码
  
    
 static void WeakReferenceCase()
    
 {
    
     var person = new Person() { ressurect = true };
    
     weakHandle = GCHandle.Alloc(person, GCHandleType.WeakTrackResurrection);
    
 }

3. coreclr源码分析

该 struct 在 coreclr 中枚举强对应 GCHandleType 作为一种结构体存在,并且其名称显得更加清晰。代码如下:

复制代码
  
    
 typedef enum
    
 {
    
 	HNDTYPE_WEAK_SHORT = 0,
    
 	HNDTYPE_WEAK_LONG = 1,
    
 }
    
 HandleType;

接下来看下刚才截图源码上的验证。

复制代码
  
    
 void gc_heap::mark_phase(int condemned_gen_number, BOOL mark_only_p)
    
 {
    
 	// null out the target of short weakref that were not promoted.
    
 	GCScan::GcShortWeakPtrScan(condemned_gen_number, max_generation, &sc);
    
  
    
 	dprintf(3, ("Finalize marking"));
    
 	finalize_queue->ScanForFinalization(GCHeap::Promote, condemned_gen_number, mark_only_p, __this);
    
  
    
 	// null out the target of long weakref that were not promoted.
    
 	GCScan::GcWeakPtrScan(condemned_gen_number, max_generation, &sc);
    
 }
    
  
    
 BOOL CFinalize::ScanForFinalization(promote_func* pfn, int gen, BOOL mark_only_p, gc_heap* hp)
    
 {
    
     for (unsigned int Seg = startSeg; Seg <= gen_segment(0); Seg++)
    
     {
    
     Object** endIndex = SegQueue(Seg);
    
     for (Object** i = SegQueueLimit(Seg) - 1; i >= endIndex; i--)
    
     {
    
         CObjectHeader* obj = (CObjectHeader*)*i;
    
  
    
         if (!g_theGCHeap->IsPromoted(obj))
    
         {
    
             if (method_table(obj)->HasCriticalFinalizer())
    
             {
    
                 MoveItem(i, Seg, CriticalFinalizerListSeg);
    
             }
    
             else
    
             {
    
                 MoveItem(i, Seg, FinalizerListSeg);
    
             }
    
         }
    
     }
    
     }
    
  
    
     if(finalizedFound) GCToEEInterface::EnableFinalization(true);
    
  
    
     return finalizedFound;
    
 }
    
    
    
    
    
![](https://ad.itadn.com/c/weblog/blog-img/images/2025-07-12/vkjbUVNgQTGe5cZoRF23M0rYIxah.png)

源码中有几个注意点:

1、如何判断一个对象为垃圾

gc 在进行标记操作时,在对象mt的第一个属性值处设置`1$以标识该对象已经被成功标记;而未被设置的对象则被视为无效或垃圾对象。

2、终结器线程真的被启动了吗

在简化后的源代码中观察到,在某个特定情况下当某个垃圾对象被分配至GC队列的预备区时,则会调用GCToEEInterface::EnableFinalization(true)来启动相关的终结er线程;因此,在测试用例中添加了GC.WaitForPendingFinalizers();这一操作是为了确保所有终结er线程都已经完成任务之后才进行目标对象(Target)的判断;这样就能提高结果的准确性。

4. 切断逻辑在哪里

有些人可能对这个具体的代码逻辑感到好奇:weakHandle.Target=null 这个操作到底是如何在core clr中执行的?我们可以用 winbg 工具中的 ba 断点功能来实现对此处逻辑的具体定位与跟踪。为了更好地理解这一过程,请参考以下示例:

文章转载自:一线码农

原文链接:https://www.cnblogs.com/huangxincheng/p/18272869

体验地址:引迈 - JNPF快速开发平台_低代码开发平台_零代码开发平台_流程设计器_表单引擎_工作流引擎_软件架构

全部评论 (0)

还没有任何评论哟~