翼度科技»论坛 编程开发 .net 查看内容

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

7

主题

7

帖子

21

积分

新手上路

Rank: 1

积分
21
一:背景

1. 讲故事

最近在分析dump时,发现有程序的卡死和WeakReference有关,在以前只知道怎么用,但不清楚底层逻辑走向是什么样的,借着这个dump的契机来简单研究下。
二:弱引用的玩法

1. 一些基础概念

用过WeakReference的朋友都知道这里面又可以分为弱短和弱长两个概念,对应着构造函数中的trackResurrection参数,同时它也是对底层GCHandle.Alloc 方法的封装,参考源码如下:
  1. public WeakReference(object? target, bool trackResurrection)
  2. {
  3.     Create(target, trackResurrection);
  4. }
  5. private void Create(object target, bool trackResurrection)
  6. {
  7.     nint num = GCHandle.InternalAlloc(target, trackResurrection ? GCHandleType.WeakTrackResurrection : GCHandleType.Weak);
  8.     _taggedHandle = (trackResurrection ? (num | 1) : num);
  9.     ComAwareWeakReference.ComInfo comInfo = ComAwareWeakReference.ComInfo.FromObject(target);
  10.     if (comInfo != null)
  11.     {
  12.         ComAwareWeakReference.SetComInfoInConstructor(ref _taggedHandle, comInfo);
  13.     }
  14. }
  15. public enum GCHandleType
  16. {
  17.     //
  18.     // Summary:
  19.     //     This handle type is used to track an object, but allow it to be collected. When
  20.     //     an object is collected, the contents of the System.Runtime.InteropServices.GCHandle
  21.     //     are zeroed. Weak references are zeroed before the finalizer runs, so even if
  22.     //     the finalizer resurrects the object, the Weak reference is still zeroed.
  23.     Weak = 0,
  24.     //
  25.     // Summary:
  26.     //     This handle type is similar to System.Runtime.InteropServices.GCHandleType.Weak,
  27.     //     but the handle is not zeroed if the object is resurrected during finalization.
  28.     WeakTrackResurrection = 1
  29. }
复制代码
从上面的 GCHandleType 的注释来看。

  • Weak 会在终结器执行之前判断持有的对象是否为垃圾对象,如果是的话直接切断引用。
  • WeakTrackResurrection 会在终结器执行之后判断对象是否为垃圾对象,如果是的话直接切断引用。
可能这么说有点抽象,画张图如下:

2. 一个简单的测试例子

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

  • Weak 的情况
因为在 ScanForFinalization 方法之前做的判断,所以与垃圾对象的联系会被马上切断,参考代码如下:
  1.     class Program
  2.     {
  3.         static void Main()
  4.         {
  5.             WeakReferenceCase();
  6.             GC.Collect();
  7.             GC.WaitForPendingFinalizers();
  8.             Console.WriteLine(weakHandle.Target ?? "Person 引用被切断");
  9.             Console.ReadLine();
  10.         }
  11.         public static GCHandle weakHandle;
  12.         static void WeakReferenceCase()
  13.         {
  14.             var person = new Person() { ressurect = false };
  15.             weakHandle = GCHandle.Alloc(person, GCHandleType.Weak);
  16.         }
  17.     }
  18.     public class Person
  19.     {
  20.         public bool ressurect = false;
  21.         ~Person()
  22.         {
  23.             if (ressurect)
  24.             {
  25.                 Console.WriteLine("Person 被永生了,不可能被消灭的。。。");
  26.                 GC.ReRegisterForFinalize(this);
  27.             }
  28.             else
  29.             {
  30.                 Console.WriteLine("Person 析构已执行...");
  31.             }
  32.         }
  33.     }
复制代码


  • WeakTrackResurrection 的情况
因为是在 ScanForFinalization 之后做的判断,这时候可能会存在 对象复活 的情况,所以垃圾又变成不垃圾了,如果是这种情况就不能切断,参考代码如下:
  1. static void WeakReferenceCase()
  2. {
  3.     var person = new Person() { ressurect = true };
  4.     weakHandle = GCHandle.Alloc(person, GCHandleType.WeakTrackResurrection);
  5. }
复制代码

3. coreclr源码分析

在 coreclr 里有一个 struct 枚举强对应 GCHandleType 结构体,而且名字看的更加清楚,代码如下:
  1. typedef enum
  2. {
  3.         HNDTYPE_WEAK_SHORT = 0,
  4.         HNDTYPE_WEAK_LONG = 1,
  5. }
  6. HandleType;
复制代码
接下来看下刚才截图源码上的验证。
  1. void gc_heap::mark_phase(int condemned_gen_number, BOOL mark_only_p)
  2. {
  3.         // null out the target of short weakref that were not promoted.
  4.         GCScan::GcShortWeakPtrScan(condemned_gen_number, max_generation, &sc);
  5.         dprintf(3, ("Finalize marking"));
  6.         finalize_queue->ScanForFinalization(GCHeap::Promote, condemned_gen_number, mark_only_p, __this);
  7.         // null out the target of long weakref that were not promoted.
  8.         GCScan::GcWeakPtrScan(condemned_gen_number, max_generation, &sc);
  9. }
  10. BOOL CFinalize::ScanForFinalization(promote_func* pfn, int gen, BOOL mark_only_p, gc_heap* hp)
  11. {
  12.     for (unsigned int Seg = startSeg; Seg <= gen_segment(0); Seg++)
  13.     {
  14.         Object** endIndex = SegQueue(Seg);
  15.         for (Object** i = SegQueueLimit(Seg) - 1; i >= endIndex; i--)
  16.         {
  17.             CObjectHeader* obj = (CObjectHeader*)*i;
  18.             if (!g_theGCHeap->IsPromoted(obj))
  19.             {
  20.                 if (method_table(obj)->HasCriticalFinalizer())
  21.                 {
  22.                     MoveItem(i, Seg, CriticalFinalizerListSeg);
  23.                 }
  24.                 else
  25.                 {
  26.                     MoveItem(i, Seg, FinalizerListSeg);
  27.                 }
  28.             }
  29.         }
  30.     }
  31.     if(finalizedFound) GCToEEInterface::EnableFinalization(true);
  32.     return finalizedFound;
  33. }
复制代码
源码中有几个注意点:

  • 如何判断一个对象为垃圾
gc 在标记时,将有根的对象mt的第一位设为 1 来表示当前已经标记过,即有用对象,未被标记的即为垃圾对象。

  • 终结器线程真的被启动了吗
从简化的源码看,一旦有垃圾对象被送入到 终结器队列的 预备区 时,就会通过 GCToEEInterface::EnableFinalization(true) 启动终结器线程,所以在测试代码中加了 GC.WaitForPendingFinalizers(); 就是为了等待终结器线程执行完毕然后才判断 Target,这样结果就会更加准确。
4. 切断逻辑在哪里

有些朋友会好奇那个 weakHandle.Target=null 的逻辑到底在 coreclr 的何处,这个比较简单,可以用 windbg 下 ba 断点即可,我们还是拿弱引用来举例,截图如下:

三:总结

WeakReference 的内部玩法有很多,更深入的理解还需要对 g_HandleTableMap 进行深度挖掘,后面有机会再聊吧,有时候dump分析还是挺苦逼的,需要对相关领域底层知识有一个足够了解,否则谈何修复呢?

来源:https://www.cnblogs.com/huangxincheng/p/18272869
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x

举报 回复 使用道具