叫我安不理

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

接上文

https://www.cnblogs.com/lmy5215006/p/18456380
评论区精彩,大佬深入讨论了C#的Finalize最佳实践,感觉有必要整理下来,拓展阅读,开拓眼界。

GC类中几个非常重要的API

  1. GC.ReRegisterForFinalize
    顾名思义,再次注册一个已经注册过的可终结对象。其底层实现逻辑与常规的终结注册过程使用同一个方法。
  2. GC.SuppressFinalize
    禁止执行对象的终结器。CLR对它进行了高度优化。前文说到,Finaze Queue的出与入。都需要移除对象并移动后续元素。而该方法仅执行一个非常高效的操作:在object header设置一个bit标记,终结器线程不会调用被bit设置过的Finalize方法。
  3. GC.WaitForPendingFinalizers
    阻塞常规线程,直到终结器线程将F-Reachable queue所有对象处理完毕。
  4. GC.KeepAlive
    延迟对象的生存期,这在控制激进式根回收有大用

确定性终结

C#引入了IDisposable接口实现确定性终结(显式清除)。作为手动调用非托管资源close/reset/release等方法的上位替代,相当于统一了抽象层。这么做有很多好处,比如代码扫描,可以轻松扫描出实现了IDisposable ,但从未调用Dispose方法的代码。再比如降低代码维护成本,只要无脑调用Disposable方法,而不是下钻到具体实现,才知道应该调用什么方法。

using

using作为语法糖,不做过多描述。在IL层中,转换成try-finally形式。在finally最末尾调用Dispose。
image

放在末尾有一个好处,避免激进式根回收造成的对象被提前回收

Disposable模式(确定性终结与非确定性终结的结合)

由于C#并没有强制要求一定要使用using或者调用Dispose方法,但人是会失误的,保不齐哪天就忘记主动释放导致了内存泄漏。所以有没有一种两全其美的办法呢?
image

点击查看代码
    public class Test : IDisposable
    {
        //正常使用dispose方法释放
        public void Dispose()
        {
            Release();
            //GC.SuppressFinalize(this);
        }
        //析构函数作为兜底
        ~Test()
        {
            Release();
        }

        //释放非托管资源的方法
        private void Release() { }
    }
但这样缺点太明显,又重新使用了终结器。这是我们不想看到的结果。 前面曾提起的GC.SuppressFinalize这时候就发挥了作用,它将以一种高性能的方式停止调用对象的终结器。

眼见为实

image
可以看到,未调用前。objech header的bit位为0,调用GC.SuppressFinalize后,bit位为0x40000000。代表禁止运行析构函数

这种使用IDisposable执行显式清除,再使用终结器执行隐式清除。合二为一的方式,被称为Disposable模式。

最佳实践

当然示例代码并不完美,如同评论区大佬所说,有重复执行的可能。
image
当然,最佳实践也出现在评论区

@花落心语提供的优秀代码
    private bool disposedValue;
    protected virtual void Dispose(bool disposing)
    {
		//双检锁,防止多次执行清理代码
        if (!disposedValue)
        {
            if (disposing)
            {
                // TODO: 释放托管状态(托管对象)
            }

            // TODO: 释放未托管的资源(未托管的对象)并重写终结器
            // TODO: 将大型字段设置为 null
            disposedValue = true;
        }
    }

    // TODO: 仅当“Dispose(bool disposing)”拥有用于释放未托管资源的代码时才替代终结器
    ~Test()
    {
         // 不要更改此代码。请将清理代码放入“Dispose(bool disposing)”方法中
         Dispose(disposing: false);
    }

    void IDisposable.Dispose()
    {
        // 不要更改此代码。请将清理代码放入“Dispose(bool disposing)”方法中
        Dispose(disposing: true);
        GC.SuppressFinalize(this);
    }

总结

image

尽管我们已经在实现上实现了“银弹”,但是请别忘记。因为析构函数的存在,依旧避免不了分配时的开销(使用慢速分支分配内存)。
它只是不再进入f-reachable queue 。但依旧会维护在finalize queue 中。因此移除出finalize queue,也是有开销的。
并且有一个大前提:在合适的时间调用GC.SuppressFinalize方法。否则将前功尽弃。
因此养成良好的习惯,时刻不忘记using对象。且借助工具协助你扫描才是真正的“银弹”。

您要是一个纯托管对象,又没特殊要求。你写析构函数图个啥?

眼见为实

点击查看代码
internal class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("start");
            Test();
            Debugger.Break();
            GC.Collect();
            //GC.Collect();
            Debugger.Break();
            Console.WriteLine("end");
            Console.ReadLine();
 
        }
        private static void Test()
        {
            var t = new SuppressDemo();
            var t2 = new SuppressDemo();
            var t3 = new SuppressDemo();

            Console.WriteLine("all obj allco. ");


            t.Dispose();
        }
    }
		
	public class SuppressDemo:IDisposable
    {
        private void Release() { }

        public void Dispose()
        {
            Release();
            GC.SuppressFinalize(this); 
        }
        public SuppressDemo()
        {
            Console.WriteLine("this is constructor. ");
        }
        ~SuppressDemo()
        {
            Console.WriteLine("this is finalize.");
            Release();
            Debugger.Break();
        }
    }
  1. 创建了3个SuppressDemo对象,不管有没有执行GC.SuppressFinalize,依旧维护在finalize queue中
    image

  2. 对象执行GC.SuppressFinalize,会发生什么?
    当调用GC.SuppressFinalize方法后,CLR底层会调用GCHeap:SetFinalizationRun方法。将对象的object header 高2位标记为0X40。当终结线程开始运作时,因为不会进入F-Reachable Queue队列。所有没有gc root。GC会直接回收,并同步移除出finalize queue

GC前
image
GC后
image

挖坑待埋

激进的根回收

对象复活

安全句柄

弱引用

posted on 2024-10-15 13:55  叫我安不理  阅读(59)  评论(0编辑  收藏  举报

导航