写点什么

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

  • 2024-09-12
    福建
  • 本文字数:2738 字

    阅读完需:约 9 分钟

一:背景


1. 讲故事


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


二:弱引用的玩法


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}
复制代码


从上面的 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 析构已执行..."); } } }
复制代码



2、WeakTrackResurrection 的情况


因为是在 ScanForFinalization 之后做的判断,这时候可能会存在 对象复活 的情况,所以垃圾又变成不垃圾了,如果是这种情况就不能切断,参考代码如下:



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



3. coreclr 源码分析


在 coreclr 里有一个 struct 枚举强对应 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;}
复制代码


源码中有几个注意点:


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


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


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


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


4. 切断逻辑在哪里


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



文章转载自:一线码农

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

体验地址:http://www.jnpfsoft.com/?from=infoq

用户头像

还未添加个人签名 2023-06-19 加入

还未添加个人简介

评论

发布
暂无评论
聊一聊 C# 弱引用 底层是怎么玩的_C#_快乐非自愿限量之名_InfoQ写作社区