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

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

一、背景

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. 一个简单的测试例子

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

  • 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 析构已执行...");
            }
        }
    }

图片

图片

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

源码中有几个注意点:

  • 如何判断一个对象为垃圾

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

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

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

4. 切断逻辑在哪里

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

图片

图片

三、总结

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

作为程序员,持续学习和充电非常重要,作为开发者,我们需要保持好奇心和学习热情,不断探索新的技术,只有这样,我们才能在这个快速发展的时代中立于不败之地。低代码也是一个值得我们深入探索的领域,让我们拭目以待,它将给前端世界带来怎样的变革。

介绍一款程序员都应该知道的软件JNPF快速开发平台,很多人都尝试用过它,它是功能的集大成者,任何信息化系统都可以基于它开发出来。

JNPF 可以实现应用从创建、配置、开发、测试到发布、运维、升级等完整生命周期的管理。减少了传统应用程序的代码编写量,通过图形化、可视化的界面,以拖放组件的方式,即可快速生成应用程序的产品,大幅降低了开发企业管理类软件的难度。

  • 9
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值