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

· 浏览次数 : 0

小编点评

本文主要讨论了弱引用(Weak Reference)的概念、原理以及与垃圾回收(Garbage Collection, GC)的关系。弱引用是一种特殊的引用类型,它可以跟踪一个对象的存在性,但不会阻止垃圾回收器回收该对象。在某些情况下,弱引用可以被复活(resume),这意味着即使对象原本应该被回收,但在某些条件下,它仍然可以被恢复。 1. **弱引用概念**: - 弱引用分为弱短(Weak Short)和弱长(Weak Long)两种。 - 弱引用在对象被回收前就会被切断引用。 - 弱长引用在对象被回收后仍然保留引用,除非被复活。 2. **弱引用测试例子**: - 使用对象复活(resume)来进行测试,观察弱引用和弱长引用的行为差异。 - 弱引用在ScanForFinalization之前切断引用,而弱长引用则可能在终结器执行后仍然保持引用。 3. **coreclr源码分析**: - 分析了coreclr中的GCHandleType枚举和相关的标记过程。 - 强调了如何通过标记过程来判断一个对象是否为垃圾。 - 描述了终结器线程的启动时机以及如何通过GC.WaitForPendingFinalizers()等待其完成。 4. **弱引用切断逻辑**: - 弱引用Target属性为null的逻辑实际上在coreclr的Ba内存分析器中使用调试器断点来实现的。 总的来说,弱引用是编程中一种重要的内存管理工具,它可以帮助开发者控制对象的生命周期,避免内存泄漏等问题。然而,要深入理解其内部机制,仍需对底层系统有更深入的了解。

正文

一:背景

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

  1. 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 来表示当前已经标记过,即有用对象,未被标记的即为垃圾对象。

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

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

4. 切断逻辑在哪里

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

三:总结

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

图片名称

与聊一聊 C# 弱引用 底层是怎么玩的相似的内容:

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

一:背景 1. 讲故事 最近在分析dump时,发现有程序的卡死和WeakReference有关,在以前只知道怎么用,但不清楚底层逻辑走向是什么样的,借着这个dump的契机来简单研究下。 二:弱引用的玩法 1. 一些基础概念 用过WeakReference的朋友都知道这里面又可以分为弱短和弱长两个概念

聊一聊如何截获 C# 程序产生的日志

一:背景 1.讲故事 前段时间分析了一个dump,一顿操作之后,我希望用外力来阻止程序内部对某一个com组件的调用,对,就是想借助外力实现,如果用 windbg 的话,可以说非常轻松,但现实情况比较复杂,客户机没有windbg,也不想加入任何的手工配置,希望全自动化来处理。 真的很无理哈。。。不过这

聊一聊对一个 C# 商业程序的反反调试

一:背景 1.讲故事 前段时间有位朋友在微信上找到我,说他对一个商业的 C# 程序用 WinDbg 附加不上去,每次附加之后那个 C# 程序就自动退出了,问一下到底是怎么回事?是不是哪里搞错了,有经验的朋友应该知道,其实这是 商业程序 的反调试机制捣鬼的,为了保护程序隐私,一般都不希望他人对自己做逆

聊一聊被 .NET程序员 遗忘的 COM 组件

一:背景 1.讲故事 最近遇到了好几起和 COM 相关的Dump,由于对 COM 整体运作不是很了解,所以分析此类dump还是比较头疼的,比如下面这个经典的 COM 调用栈。 0:044> ~~[138c]s win32u!NtUserMessageCall+0x14: 00007ffc`5c891

.NET周报【10月第3期 2022-10-25】

国内文章 聊一聊被 .NET程序员 遗忘的 COM 组件 https://www.cnblogs.com/huangxincheng/p/16799234.html 将Windows编程中经典的COM组件拿出来再复习一下,解释了COM组件互相调用的原理。 使用 C# 开发的轻量级开源数据库 Lite

浅聊一下 C#程序的 内存映射文件 玩法

## 一:背景 ### 1. 讲故事 前段时间训练营里有朋友问 `内存映射文件` 是怎么玩的?说实话这东西理论我相信很多朋友都知道,就是将文件映射到进程的虚拟地址,说起来很容易,那如何让大家眼见为实呢?可能会难倒很多人,所以这篇我以自己的认知尝试让大家眼见为实。 ## 二:如何眼见为实 ### 1.

记一次 Windows10 内存压缩模块 崩溃分析

一:背景 1. 讲故事 在给各位朋友免费分析 .NET程序 各种故障的同时,往往也会收到各种其他类型的dump,比如:Windows 崩溃,C++ 崩溃,Mono 崩溃,真的是啥都有,由于基础知识的相对缺乏,分析起来并不是那么的顺利,今天就聊一个 Windows 崩溃的内核dump 吧,这个 dum

聊一聊领域驱动与贫血模型

写在前面 前段时间跟领导讨论技术债概念时不可避免地提到了代码的质量,而影响代码质量的因素向来都不是单一的,诸如项目因素、管理因素、技术选型、人员素质等等,因为是技术债务,自然就从技术角度来分析,单纯从技术角度来看代码质量,其实又细分很多原因,如代码设计、代码规范、编程技巧等等,但我个人觉得这些都是技

聊一聊 Monitor.Wait 和 Pluse 的底层玩法

一:背景 1. 讲故事 在dump分析的过程中经常会看到很多线程卡在Monitor.Wait方法上,曾经也有不少人问我为什么用 !syncblk 看不到 Monitor.Wait 上的锁信息,刚好昨天有时间我就来研究一下。 二:Monitor.Wait 底层怎么玩的 1. 案例演示 为了方便讲述,先

聊一聊Java中的Steam流

在我们的日常编程任务中,对于集合的制造和处理是必不可少的。当我们需要对于集合进行分组或查找的操作时,需要用迭代器对于集合进行操作,而当我们需要处理的数据量很大的时候,为了提高性能,就需要使用到并行处理,这样的处理方式是很复杂的。流可以帮助开发者节约宝贵的时间,让以上的事情变得轻松。