在探讨这个技术之前, 我们先看一段代码:
1 . 这段代码就是窗体加载时利用 File 静态类实现创建一个 txt 文件
2 . 随后点击窗体上的 button 创建一个针对以上创建 txt 文件的文件流对象
我们来运行看看, 首先弹出 Demo 窗体, 这是可以看到桌面上新建了一个 text.txt 的文本文件, 随后我们点击窗体上的 button1 的按钮之后
这时有经验的小伙伴可能要说了, File 打开了没有释放资源, 要先释放资源.
如果我在原先代码中增加如下代码之后呢?
运行之后发现, Demo 可以正常运行, 并没有报任何异常了.
接下来, 我们不增加任何代码, 只是更改下 Test() 在代码中的位置
运行之后, 报了同样的异常:
我的天啊! 这是咋回事?
接下来就引出今天的技术主题, 希望通过本章的探究之后, 小伙伴们可以明白这是怎么回事儿......
一. 何为垃圾回收 (GC-Garbage Collector)
物理内存: 存放应用程序运行期间所产生的也是必须的信息资源, 也即是二进制信息的集合. 内存是宝贵的资源, 好东西当然要用到刀刃上, 经不起浪费, 如果不处理好垃圾回收, 就会时常遇到 OutOfMemory 的报错 - 内存溢出, 这对操作系统以及应用程序的使用是极大的伤害, 所以及时回收垃圾内存是必不可少的关键机制.
二. 垃圾回收机制
内存资源分配
托管资源 (栈, 托管堆 CLR-GC 自动回收, 是由操作系统决定回收时机)
非托管资源 (非托管堆 Follow C/C++ 的手动释放)
托管资源回收机制
.Net 中 80% 都是托管资源, 比如为我们所熟知的值类型与引用类型, 而值类型是直接分配在内存的栈区域, 这块区域的内存是用完即弹出的, 所以不需要任何额外的工作去参与回收, 而引用类型是分配在内存的托管堆区域, 这块区域是由 CLR 负责分配与回收的. 这里就要让大名鼎鼎的 GC(垃圾回收器) 出场了, 首先 GC 是系统级的一个线程, 即它是由系统来调用, 那么系统何时回去调用这个 GC 呢? 一定是有某种机制来保证这个功能的运作, 对的, 那就是 GC 首先会去扫描托管堆内存中的所有对象引用, 只要某对象不可达 (即没有任何 root 引用到该对象), 那么这个对象就会被标记 (标记为垃圾回收的目标), 在这个 GC 机制中有一个很重要的概念那就是 GC 的代 (就是等级的意思), 这个代的机制的引入主要是为了提高性能, 以避免每次回收整个托管堆造成的性能损失, 这里具体就不介绍了. 最后总结一下 GC 的特性:
1) GC 只会自动管理托管内存资源的回收, 它是不能够自动管理释放非托管资源的;
2) GC 并不是实时性的, 这将会造成系统性能上的拼瓶颈与不确定性.
非托管资源回收机制
其他资源比如窗口句柄, 数据库连接, 字节流, 文件流, GDI + 相关对象, COM 对象, Pen 等等是属于非托管资源, 这里需要注意的是, 为啥数据库连接我没说 SqlConnection, 文件流我没说 FileStream, 字节流我没说 BinaryStream 等等, 其实严格意义上来说, SqlConnection,FileStream,BinaryStream 之类的并不能称之为非托管资源, 其实他们是托管类, 但是这些托管类当中却使用了非托管资源, 所以就资源来说, 就是数据库连接, 文件流, 字节流. 而 SqlConnection,FileStream,BinaryStream 就是使用了非托管资源的托管类. 在我们实际开发过程中, 更多遇到的就是这些使用非托管资源的托管类 (所以后续所谈的关于非托管资源回收也就是针对这种情况). 单纯纯粹的使用那些非托管资源是很少的, 这些非托管资源是分配在非托管内存中, 而不是前面所说的托管内存中, 所以非托管资源的回收 GC 是无法插手的, 那这就得有程序自己去做好回收处理了. 那么牛逼的. Net Framework 有没有提供给我们释放非托管资源的方式呢, 很显然, MS 是不会让我们失望的, 她提供了 2 种方式, 一种就是类型自带 Finalize() 方法, 另一种就是实现 IDisposable 接口的 Dispose 方法, 相较于 GC 来说, 非托管资源的回收权掌握在我们自己手里, 那么我们可就要好好捣鼓捣鼓, 要不然没强大的 GC 给我们擦屁股, 我们自己是很容易犯错的, 动不动你可能就会遇到你的应用程序内存暴涨, 性能低下甚至程序无故崩溃的恶心后果. 所以接下来我们就来分析分析这两种方式的使用:
1) Finalize 方法
在. Net 的基类 System.Object 中, 定义了名为 Finalize() 的一个虚方法, 这个方法默认啥都不做.
顾名思义 Finalize: 终结的意思, 即指工作收尾, 清场的意思. 所以很显然这个函数就是提供我们清理资源的一个入口, 那么这个方法是谁去调用的呢? 是系统有个机制去调用亦或是我们程序自己去调用呢? 很开心的告诉你, 是系统去调用, 小伙伴们听到后很开心有木有, 终于又可以省下一笔时间好好的喝喝茶看看报了顺带玩把跳一跳了 _.(凡事都有两面性哦, 正因为是操作系统做, 那就不敢保证实时性与确定性喽,.Net 大大很给力的, 后面又提供了另外一种方式, 对啦, 就是后面我们将要讲的 IDisposable)
言归正传, 那系统又是如何调用 Finalize 方法的呢, 所以下面我们来谈谈 Finalize 的工作机制:
i) CLR 在托管堆上分配对象空间的时候, 会自动确定该对象是否提供一个自定义的 Finalize 方法, 如果检测到有的话, 那么这个对象就会被标记为可终结的, 同时一个指向这个对象的指针就会被保存到一个名字为终结队列的内部队列中, 终结队列是有 GC 维护的一张表 (小伙伴们是不是很亲切啊, 对的, 看到 GC 啦), 这种表指向每一个在从堆上删除之前必须终结的对象.
ii) 当 GC 确定要从内存中释放某个对象的时候, 它会检查终结队列上的每一项, 并将对象放到一个队列中 (从终结队列移到 foreachable 队列) 中去, 然后启动另外一个独立线程 (我们称之为 Finalizer 线程) 而不是 GC 线程来执行这些 Finalizer(下个 GC 周期时),GC 线程会继续删除其他待回收的对象, 而是在下一个 GC 周期, Finalizer 线程才去回收这些对象, 由此可见, 实现了 Finalize 方法的对象必须等待两次 GC 才能被完全释放, 所以这些对象某种意义上是会在 GC 中自动 "延长" 生存周期. 从上面可以看出, Finalize 方法的调用是蛮耗费资源的, Finalize 方法的作用是保证. Net 对象能够在垃圾回收时清理非托管资源, 如果创建了一个不使用非托管资源的类型, 实现终结器是没有任何意义的, 所以没有特殊的需求应该要避免重写 Finalize 方法.
看到这, 是不是有一些好学的小伙伴屁颠屁颠的跑去 VS 上给某个使用了非托管资源的类型重新 Finalize 方法, 一编译, 卧槽, 编译失败
其实, 当我们想重写 Finalize 方法时, C# 为我们为我们提供了析构函数这种语法来重写该方法, 为毛要这样曲折呢, 感兴趣的朋友可以研究研究 (也可以在文章结尾处多注意注意哈_), 析构函数语法跟构造函数类似, 但析构函数有个前缀~, 并且不能加任何访问修饰符, 不能加任何参数, 不能重载, 所以一个类只能有一个析构函数, 也叫终结器.
2) IDisposable 接口
记性好的小伙伴们应该还记得上文有提到过这个茬, 那就是通过垃圾回收是可以利用对象的终结器来释放非托管资源. 然后, 很多非托管资源非常宝贵, 比如数据库连接以及文件句柄, 所以他们应该尽可能快的被回收资源, 而不能依靠垃圾回收来被动处理, 为了更及时的对这些非托管资源进行回收, 进而. Net 提供了另外一种方式 - IDisposable 接口, 跟垃圾回收的被动处理不同, 此接口是提供给了我们主动回收的方式, 这样就能如我们所愿主动及时的去回收那些非托管资源了, 哈哈, 我又要说那句富有哲理的老话啦, 凡事都有两面性的, 小伙伴们是不是都有想打我的冲动啦, Are you kidding us??? 小伙伴们稍安勿躁, 人生在世, 切忌浮躁哦, 人生就是这样, 凡事都有好有坏, 世事无常, 找到一个合适的平衡点对于人生是很重要的...... 扯远啦, 回到正题来, 为什么说这种方式也具有两面性呢, 因为这种方式是我们自己显式去调用, 是人那就会犯错, 所以丢掉忘记那是很有可能的事, 可能会漏掉 Dispose 的调用也有可能是在调用 Dispose 之前出现了异常, 那么有些资源可能就一直留在内存中了, 除非你通过工具手动清除或重启电脑, 为最大程度的避免这种疏忽, 我们可以使用 try catch finally 这种方式保证 Dispose 确实会被调用到, 但每次套个 try catch finally 会觉得很麻烦, 故此 C# 为我们提供了 using 关键字来简化 Dispose 的调用, 其实实质上就是 try catch finally 的模式, 只不过 C# 做了语法糖, 让我们写起来更简洁, 所以任何实现了 IDisposable 接口的类型, 都可以用 using 语句, 没有的话, 那直接就会编译报错啦.
从前面的介绍了解到, Finalize 可以通过垃圾回收进行自动的调用, 而 Dispose 需要被代码显示的调用, 所以, 为了保险起见, 对于一些非托管资源, 还是有必要实现终结器的. 也就是说, 如果我们忘记了显示的调用 Dispose, 那么垃圾回收也会调用 Finalize, 从而保证非托管资源的回收.
其实, MSDN 上给我们提供了一种很好的模式来实现 IDisposable 接口来结合 Dispose 和 Finalize, 例如下面的代码:
class MyResourceWrapper : IDisposable
{
private bool IsDisposed = false;
public void Dispose()
{
Dispose(true);
//tell GC not invoke Finalize method
GC.SuppressFinalize(this);
}
protected void Dispose(bool Disposing)
{
if (!IsDisposed)
{
if (Disposing)
{
//clear managed resources
}
//clear unmanaged resources
}
IsDisposed = true;
}
~MyResourceWrapper()
{
Dispose(false);
}
}
在这个模式中, void Dispose(bool Disposing) 函数通过一个 Disposing 参数来区别当前是否是被 Dispose() 调用. 如果是被 Dispose() 调用, 那么需要同时释放托管和非托管的资源. 如果是被终结器调用了, 那么只需要释放非托管的资源即可. Dispose() 函数是被其它代码显式调用并要求释放资源的, 而 Finalize 是被 GC 调用的.
另外, 由于在 Dispose() 中已经释放了托管和非托管的资源, 因此在对象被 GC 回收时再次调用 Finalize 是没有必要的, 所以在 Dispose() 中调用 GC.SuppressFinalize(this) 避免重复调用 Finalize. 同样, 因为 IsDisposed 变量的存在, 资源只会被释放一次, 多余的调用会被忽略.
所以这个模式的优点可以总结为:
如果没有显示的调用 Dispose(), 未释放托管和非托管资源, 那么在垃圾回收时, 还会执行 Finalize(), 释放非托管资源, 同时 GC 会释放托管资源
如果调用了 Dispose(), 就能及时释放了托管和非托管资源, 那么该对象被垃圾回收时, 就不会执行 Finalize(), 提高了非托管资源的使用效率并提升了系统性能
通过以上的探究, 现在回到文章一开始遇到的那个问题, 我们就可以知道实际上 File.Create 方法返回的是一个 FileStream 实例:
然而这个实例其实就是一个使用了非托管资源的托管类, 而文章一开始的例子当中, 在创建完 File 之后并没有及时的去回收掉这个 FileStream 实例, 所以他只能等待 GC 的自动回收, 然后 GC 的回收机制是不实时和不确定的, 所以当我们紧接着去针对这个文件创建一个文件流的时候 GC 此时还并没有去回收她, 所以就会出现占用的异常, 而增加 Test() 这个方法主要是为了故意增加内存的使用, 逼迫系统进行一次垃圾回收 (当然我们也可以通过 GC.Collect() 来做), 所以之后就不会再出现这个异常, 而为什么将 Test() 代码位置变动一下之后也会出现异常呢, 这个就是上面提到的 GC 代的概念, 有兴趣的朋友可以自行了解下.
末尾彩蛋: 之所以 C# 只支持这种方式进行 Finalize 方法的重写, 是因为 C# 编译器会为 Finalize 方法隐式地加入一些必需的基础代码. 下面就是我们通过 ILSpy 查看到了 IL 代码, Finalize 方法作用域内的代码被放在了一个 try 块中, 然后不管在 try 块中是否遇到异常, finally 块保证了 Finalize 方法总是能够被执行.
/** 以上仅为个人学习总结, 转载请标注原处, 如有不足之处请指正
搞技术, 我们是认真的.*****/
来源: http://www.jianshu.com/p/d1bc22719c3a