资源分类
- 托管资源
- 非托管资源
托管资源
托管资源在.NET Framework中又分别存放在两种地方:堆栈和托管堆。所有的值类型和引用类型的引用都存放在堆栈中,而所有引用所代表的对象实例都保存在堆中。
堆栈的资源出了作用域后会自动释放所占内存;托管堆由CLR(通用语言运行时)的管理,达到一定条件时由垃圾回收机制自动回收资源。
非托管资源
非托管资源包括文件句柄、数据库连接字符串、端口或者其他有限的资源。
非托管资源需要手动回收,常见的有终结器(finalizer)和Dispose。
垃圾回收(Garbage Collect,即GC)
GC是.NET Framework等CLI机制的重要部分。基本原理就是使用分代算法等优化方案遍历托管堆中的对象,对还被引用的对象做标记,然后把还在使用的对象转移到一个连续的地址空间,对其余没在使用的对象内存进行回收。
回收步骤
- 标记。根据应用程序根指针Root遍历堆上的每一个引用对象,对于还在使用的对象进行标记(其实就是在对象同步索引块中开启一个标示位)。
- 清除。对所有没有标记的对象进行清除操作,其中普通对象直接回收内存,而对于实现了终结器的对象需要单独回收处理,第一次回收调用终结器,没有清理对象,第二次回收才真正清理对象。
- 整理。垃圾回收后使得内存不连续,出现许多内存碎片,需要把剩下的对象转移到一个连续的内存并更新对象的引用。
分代算法
如果回收垃圾时每次都要遍历托管堆中所有的对象实例,那么性能消耗非常大。分代算法就是垃圾回收机制的一种优化方案。
- 第0代:一个新对象被实例化并第一次被分配在托管堆上时,就是第0代。
- 第1代:0代满了会触发0代的垃圾回收,0代垃圾回收后,剩下的对象就是第1代。
- 第2代:当0代、1代满了,会触发0代、1代的垃圾回收,第0代升为第1代,第1代升为第2代。
终结器
终结器类似于C++中的析构函数,格式如下,
Class TestFinalizer
{
...
~TestFinalizer()
{
//这里进行回收
}
...
}
- 终结器由垃圾回收器负责调用,不能在编译时确定执行终结器的时间,终结器会在对象最后一次使用之后,并在应用程序正常关闭前的某个时间调用。
- 终结器不允许传递任何参数,不能重载。
垃圾回收时对终结器的处理
- 当CLR在托管堆上分配对象时,GC检查该对象是否实现了终结器。如果是,对象会被标记为可终结的,同时这个对象的指针被保存在名为终结队列的内部队列中。终结队列是一个由垃圾回收器维护的表,它指向每一个在从堆上删除之前必须被终结的对象。
- 当GC执行并且检测到一个不被使用的对象时,需要进一步检查终结队列来查询该对象类型是否含有终结器,如果没有则将该对象视为垃圾,如果存在则将该对象的引用移动到另外一张Freachable列表,此时对象会被复活一次。
- CLR将有一个单独的高优先级线程负责处理Freachable列表,就是依次调用其中每个对象的Finalize方法,然后删除引用,这时对象实例就被视为不再被使用,对象再次变成垃圾。
- 下一个GC执行时,将释放已经被调用Finalize方法的那些对象实例。
优缺点
- 优点:自动调用。
- 缺点:性能不好,容易被滥用;非确定性终结;资源没有立即回收;终结器会导致对象复活一次,被GC回收两次才最终完成回收工作。
Dispose
Dispose的实现方式就是实现IDisposable接口并重写Dispose(),格式如下
public class TestIDispose: IDisposable
{
public FileStream stream;
public void Dispose()
{
if (stream!= null)
{
stream.Close();
}
}
}
调用方式
Dispose需要手动调用,有两种手动调用方式。
1. 显示接口调用
TestDispose obj=new TestDispose();
obj.Dispose();
缺点是很容易忘记调用,导致资源没有及时被释放。
2. using()语法调用,自动执行Dispose接口
using (var obj = new TestDispose())
{
...
}
using() 只是一种语法形式,其本质还是try…finally的结构,可以保证Dispose始终会被执行。
优缺点
- 优点:确定性终结,一旦调用立即回收。
- 缺点:需要手动调用,一旦忘记调用就会导致资源没有及时被释放
参考链接: