如果你经常看开源项目的源码,你会发现很多Dispose方法中都有这么一句代码:GC.SuppressFinalize(this);,看过一两次可能无所谓,看多了就来了兴趣,这篇就跟大家聊一聊。
一:背景
1.在哪发现的
相信现在Mysql在.Net领域中铺的面越来越广了,C#对接MySql的MySql.Data类库的代码大家可以研究研究,几乎所有操作数据库的几大对象:MySqlConnection,MySqlCommand,MySqlDataReader以及内部的Driver都存在GC.SuppressFinalize(this)代码。
2.GC.SuppressFinalize场景在哪里
先看一下官方对这个方法的解释,如下所示:
意思就是说:请求CLR不要调用指定对象的终结器,如果你对终结器的前置基础知识不足,那这句话肯定不是很明白,既然都执行了Dispose,说明非托管资源都被释放了,怎么还压制CLR不要调用Finalize呢?删掉和不删掉这句代码有没有什么严重的后果,GC类的方法谁也不敢动哈。。。为了彻底讲清楚,有必要说一下Finalize整个原理。
二:资源管理
我们都知道C#是一门托管语言,它的好处就是不需要程序员去关心内存的分配和释放,由CLR统一管理,这样编程门槛大大降低,天下攘攘皆为利来,速成系的程序员就越来越多~
1.对托管资源和非托管资源理解
1托管资源
这个很好理解,你在C#中使用的值类型,引用类型都是统一受CLR分配和GC清理。
2非托管资源
在实际业务开发中,我们的代码不可能不与外界资源打交道,比如说文件系统,外部网站,数据库等等,就拿写入文件的StreamWriter举例,如下代码:
为什么能够写入文件?那是因为我们的代码是请求windows底层的Win32Api帮忙写入的,这就有意思了,因为这个场景有第三者介入,sw是引用类型受CLR管理,win32api属于外部资源和.Net一点关系都没有,如果你在用完sw之后没有调用close方法的话,当某个时候GC回收了托管堆上的sw后,这给被打开的win32api文件句柄再也没有人可以释放了,资源就泄露了,如果没看懂,我画张图:
三:头疼的非托管资源解决方案
很多时候程序员就是在使用完类之后因为种种原因忘记了手动执行Close方法造成了资源泄露,那有没有一种机制可以在GC回收堆对象的时候回调我的一个自定义方法呢?如果能实现就了,这样我就可以在自定义方法中做全局的控制。其实这个自定义方法就是析构函数,接下来我把上面的StreamWriter改造下,将Close()方法放置在析构函数中,先看一下代码:
四:析构函数被执行的底层原理分析
让GC来通知我的回调方法这本身就很,但仔细想想,在垃圾回收时,CLR不是将所有线程都挂起了吗?怎么还有活动的线程,而且这个线程是来自哪里?线程池吗?好,先从理论跟和大家分析一下,析构函数在CLR层面称为Finalize方法,为了方便后面通过windbg去验证,这里统一都叫Finalize方法,提前告知。
1.原理步骤
1CLR在启动时会构建一个“Finalize全局数组”和“待处理Finalize数组”,所有定义Finalize方法的类,它的引用地址全部额外再灌到“Finalize全局数组”中。
2CLR启动一个专门的“Finalize线程”让其全权监视“待处理Finalize数组”。
3GC在开启清理前标记对象引用时,如发现某一个对象只有一个在Finalize数组中的引用,说明此对象是垃圾了,CLR将该对象地址转移到另外一个“待处理Finalize”数组中。
4由于该对象还存在引用,所以GC放了一马,然后“Finalize线程”监视到了“待处理Finalize数组”新增的对象,取出该对象并执行该对象的Finalize方法。
5由于是破坏性取出,此时该对象再无任何引用,下次GC启动时就会清理出去。
看文字有点绕,我画一张图帮大家理解下。
2.windbg验证
1修改Main代码如下,抓一下dump文件看看MyStreamWriter是否在Finalize全局数组中。
很惊喜的看到MyStreamWriter就在其中,符合图中所示。
2查看是否有专门的“Finalize线程”,可以通过!threads命令查看。
看到没,线程2标记了MTA(Finalizer),说明果然有执行Finalizer方法的专有线程。
3由于水平有限,不知道怎么去看“待处理Finalize数组”,所以只能验证等GC回收之后,看下“Finalize全局数组”中是否还存在MyStreamWriter即可。
可以看到这时候“全局数组”没有引用了,再看一下托管堆是否还存在MyStreamWriter以及线程栈中是否还有对象引用地址。
可以看到MyStreamWriter还是存在于托管堆,但是线程栈已再无它的引用地址,就这样告别了全世界,下次GC启动就要被彻底运走了。
如果你看懂了上面Finalize原理,再来看SuppressFinalize的解释:‘请求CLR不要调用指定对象的终结器’。就是说当你手动调用Dispose或者Close方法释放了非托管资源后,通过此方法强制告诉CLR不要再触发我的析构函数了,否则再执行析构函数相当于又做了一次清理非托管资源的操作,造成未知风险。
好了,本篇就说这么多,希望你对有帮助。