• 一张图带你了解.NET终结(Finalize)流程


    简介

    "终结"一般被分为确定性终结(显示清除)与非确定性终结(隐式清除)

    1. 确定性终结主要
      提供给开发人员一个显式清理的方法,比如try-finally,using。
    2. 非确定性终结主要
      提供一个注册的入口,只知道会执行,但不清楚什么时候执行。比如IDisposable,析构函数。

    为什么需要终结机制?

    首先纠正一个观念,终结机制不等于垃圾回收。它只是代表当某个对象不再需要时,我们顺带要执行一些操作。更加像是附加了一种event事件。
    所以网络上有一种说法,IDisposable是为了释放内存。这个观念并不准确。应该形容为一种兜底更为贴切。
    如果是一个完全使用托管代码的场景,整个对象图由GC管理,那确实不需要。在托管环境中,终结机制主要用于处理对象所持有的,不被GC和runtime管理的资源。
    比如HttpClient,如果没有终结机制,那么当对象被释放时,GC并不知道该对象持有了非托管资源(句柄),导致底层了socket连接永远不会被释放。

    如前所述,终结器不一定非得跟非托管资源相关。它的本质是”对象不可到达后的do something“.
    比如你想收集对象的创建与删除,可以将记录代码写在构造函数与终结器中

    终结机制的源码

    源码
    namespace Example_12_1_3
    {
        internal class Program
        {
            static void Main(string[] args)
            {
                TestFinalize();
    
                Console.WriteLine("GC is start. ");
                GC.Collect();
                Console.WriteLine("GC is end. ");
                Debugger.Break();
    
                Console.ReadLine();
                Console.WriteLine("GC2 is start. ");
                GC.Collect();
                Console.WriteLine("GC2 is end. ");
                Debugger.Break();
                Console.ReadLine();
    
            }
            static void TestFinalize()
            {
                var list = new List(1000);
                for (int i = 0; i < 1000; i++)
                {
                    list.Add(new Person());
                }
    
                var personNoFinalize = new Person2();
                Console.WriteLine("person/personNoFinalize分配完成");
    
                Debugger.Break();
            }
        }
        public class Person
        {
            ~Person()
            {
                Console.WriteLine("this is finalize");
                Thread.Sleep(1000);
            }
        }
        public class Person2
        {
    
        }
    }
    
    IL
    	// Methods
    	.method family hidebysig virtual 
    		instance void Finalize () cil managed 
    	{
    		.override method instance void [mscorlib]System.Object::Finalize()
    		// Method begins at RVA 0x2090
    		// Header size: 12
    		// Code size: 30 (0x1e)
    		.maxstack 1
    
    		IL_0000: nop
    		.try
    		{
    			// {
    			IL_0001: nop
    			// Console.WriteLine("this is finalize");
    			IL_0002: ldstr "this is finalize"
    			IL_0007: call void [mscorlib]System.Console::WriteLine(string)
    			// Console.ReadLine();
    			IL_000c: nop
    			IL_000d: call string [mscorlib]System.Console::ReadLine()
    			IL_0012: pop
    			// }
    			IL_0013: leave.s IL_001d
    		} // end .try
    		finally
    		{
    			// (no C# code)
    			IL_0015: ldarg.0
    			IL_0016: call instance void [mscorlib]System.Object::Finalize()
    			IL_001b: nop
    			IL_001c: endfinally
    		} // end handler
    
    		IL_001d: ret
    	} // end of method Person::Finalize
    
    汇编
    0199097B  nop  
    0199097C  mov         ecx,dword ptr ds:[4402430h]  
    01990982  call        System.Console.WriteLine(System.String) (72CB2FA8h)  
    01990987  nop  
    01990988  call        System.Console.ReadLine() (733BD9C0h)  
    0199098D  mov         dword ptr [ebp-40h],eax  
    01990990  nop  
    01990991  nop  
    01990992  mov         dword ptr [ebp-20h],offset Example_12_1_3.Person.Finalize()+045h (00h)  
    01990999  mov         dword ptr [ebp-1Ch],0FCh  
    019909A0  push        offset Example_12_1_3.Person.Finalize()+06Ch (019909BCh)  
    019909A5  jmp         Example_12_1_3.Person.Finalize()+057h (019909A7h)  
    
    可以看到,C#的析构函数只是一种语法糖。IL重写了System.Object.Finalize方法。在底层的汇编中,直接调用的就是Finalize()

    终结的流程

    image

    补充一个细节,实际上f-reachable queue 内部还分为Critical/Normal两个区间,其区别在于是否继承自CriticalFinalizerObject。
    目的是为了保证,即使在AppDomain或线程被强行中断的情况下,也一定会执行。
    一般也很少直接继承CriticalFinalizerObject,更常见是选择继承SafeHandle.
    不过在.net core中区别不大,因为.net core不支持终止线程,也不支持卸载AppDomain。

    眼见为实

    使用windbg看一下底层。
    1. 创建Person对象,是否自动进入finalize queue?
    image

    可以看到,当new obj 时,finalize queue中已经有了Person对象的析构函数

    2. GC开始后,是否移动到F-Reachable queue?
    image

    可以看到代码中创建的1000个Person的析构函数已经进入了F-Reachable queue

    sosex !finq/!frq 指令同样可以输出

    3. 析构对象是否被"复活"?
    GC发生前,在TestFinalize方法中创建了两个变量,person=0x02a724c0,personNoFinalize=0x02a724cc。
    image
    image
    可以看到所属代都为0,且托管堆中都能找到它们。

    GC发生后
    image
    image
    image
    可以看到,Person2对象因为被回收而在托管堆中找不到了,Person对象因为还未执行析构函数,所以还存在gcroot 。因此并未被回收,且内存代从0代提升到1代

    4. 终结线程是否执行,是否被移出F-Reachable queue
    image
    image
    在GC将托管线程从挂起到恢复正常后,且F-Reachable queue 有值时,终结线程将乱序执行。
    并将它们移出队列
    image

    5. 析构函数的对象是否在第二次GC中释放?
    等到第二次GC发生后,由于对象析构函数已经被执行,不再拥有gcroot,所以托管堆最终释放了该对象,
    image

    6. 析构函数如果没有及时执行完成,又触发了一次GC。会不会再次升代?
    image
    答案是肯定的

    Finaze Queue/F-Reachable Queue 底层结构

    image

    眼见为实

    image
    每个不同的代,维护在不同的内存地址中,但彼此之间的内存地址又紧密联系在一起。

    image
    与GC代优点细微区别的是,没有LOH概念,大对象分配在0代中。Person3对象是一个 new byte[8500000]。 其他行为与GC代保持一致

    终结的开销

    1. 如果一个类型具有终结器,将使用慢速分支执行分配操作
      且在分配时还需要额外进入finalize queue而引入的额外开销
    2. 终结器对象至少要经历2次GC才能够被真正释放
      至少两次,可能更多。终结线程不一定能在两次GC之间处理完所有析构函数。此时对象从1代升级到2代,2代对象触发GC的频率更低。导致对象不能及时被释放(析构函数已经执行完毕,但是对象本身等了很久才被释放)。
    3. 对象升代/降代时,finalize queue也要重复调整
      与GC分代一样,也分为3个代和LOH。当一个对象在GC代中移动时,对象地址也需要也需要在finalization queue移动到对应的代中.
      由于finalize queue与f-reachable queue 底层由同一个数组管理,且元素之间并没有留空。所以升代/降代时,与GC代不同,GC代可以见缝插针的安置对象,而finalize则是在对应的代末尾插入,并将后面所有对象右移一个位置

    眼见为实

    点击查看代码
        public class BenchmarkTester
        {
            [Benchmark]
            public void ConsumeNonFinalizeClass()
            {
                for (int i = 0; i < 1000; i++)
                {
                    var obj = new NonFinalizeClass();
                    obj.Age = i;
    
                }
            }
            [Benchmark]
            public void ConsumeFinalizeClass()
            {
                for (int i = 0; i < 1000; i++)
                {
                    var obj = new FinalizeClass();
                    obj.Age = i;
    
                }
            }
        }
    

    image

    非常明显的差距,无需解释。

    总结

    使用终结器是比较棘手且不完全可靠。因此最好避免使用它。只有当开发人员没有其他办法(IDisposable)来释放资源时,才应该把终结器作为最后的兜底

  • 相关阅读:
    翻金币项目 QT项目 (利用Qt 5.80 实现 )
    如何修改第三方DLL文件名
    30-浅拷贝和深拷贝
    webpack -v报错:Cannot find module ‘webpack-cli/package.json‘
    Outlook无需API开发连接钉钉群机器人,实现新增会议日程自动发送群消息通知
    I/O流(C++)
    redis(3)-hiredis-API函数的调用
    关于青语言语法设计的讨论
    电磁兼容(EMC)的标准与测试内容(三)
    数商云:东方甄选品控翻车,如何通过供应链建设打造可持续商业模式
  • 原文地址:https://www.cnblogs.com/lmy5215006/p/18456380