资源回收与GC
托管堆内存管理策略与垃圾回收
托管堆将对象依次保存,垃圾回收之后进行一次内存整理,所以如果GC的频繁发生会降低系统性能,因为GC对内存的清理造成对象在内存中的移动,在整个GC结束之前,CLR上的应用程序不可能继续运行,.net 使用世代垃圾回收器,CLR上的内存管理策略与OS的内存管理策略类似(CLR本身也就是虚拟机)。
1.CLR内存管理
CLR的托管堆的将对象依次保存在内存上,如果有新的内存请求,会在托管堆中遍历,找到第一个大小符合要求的连续内存空间,将对象保存在该空间。
如图,假设CLR从内存为4001的存储块(不知道CLR是不是使用分页系统,所以只能用存储块来描述)开始寻址,此时需要分配一个大小为2个单位的内存给新的对象,则CLR会由上向下遍历,找到4004-4005连续空间可用,则将新的对象保存在这块连续空间上。
2.垃圾回收与内存整理
如图,GC运行时会扫描其托管堆,查看是否有没有被引用的对象,如在GC运行时发现对象1的所用引用已经被移除,将在维护的内存表中对4001,4002,4003三个内存块的标志记为未占用,然后GC会对托管内存进行整理
其实CLR的GC回收器是一个世代回收器即:CLR将其托管堆的内存分为多个世代,从第0代开始,新申请的对象都会被分配在第0代的内存空间上,如果第0代上发生过一次GC回收,或第0代的内存空间不够(此时也将引发一次GC回收),CLR会在GC回收完成之后,将第0代上仍有的对象压缩存储并移至下一代(第1代),以此类推,原先第1代上已有的对象会被移至第2代....
这样CLR实际上保证了第0代上的对象都是最新的对象,而世代越大的空间上保存的对象就应该是越老的对象,一般最新的对象是最常修改的对象,所以在第0代上GC会执行的最有效率,内存的移动也会更快
非托管资源的回收
1.析构函数
在析构函数中标识该对象在释放时应该释放的非托管资源,代码类似
class MyClass
{
~MyClass()
{
//释放非托管资源,如数据库连接,文件,网络,流等
}
}
使用析构函数的缺点:
1.CLR不能保证GC会在何时执行,也就是说非托管资源不会在对象没有用后立即释放,而是需要等待GC的运行
2.GC在第一次调用析构函数时不会真正释放非托管资源,而是在第二次调用时在真正释放非托管资源
3.CLR使用一个独立的线程来调用所有的析构函数,如果释放资源过多会严重影响该线程的执行效率
2.IDisponse接口
IDisponse接口具有C#语言级的支持,,实现了IDispone接口的对象在使用时可以使用using来自动释放其托管资源 (等价于try.finally语句块),它的使用如下
class MyClass : IDisponse
{
public void Disponse()
{
//释放非托管资源
}
}
class UseClass
{
using(MyClass myClass = new MyClass())
{
//使用MyClass
}
}
使用IDisponse的缺点
IDispone过于依赖人的调用,如果软件开发过程中忘记了使用using或显示调用,则非托管资源得不到有效的释放
3.综合
综合使用上面的两种方法可以得到一个比较好的效果,代码类似于
public MyClass : IDispose
{
private void isDisposed = false;//记录是否已经对资源进行过释放
//实现IDispone的方法
public void Dispose()
{
Dispose(true);
GC.SuppressFinallize(this);//在执行GC时当前类的析构函数不用再调用
}
protected virtual void Disponse(bool disposing)
{
if(!isDisposed)
{
if(disposing)
{
//使用Dispose时可以对一些托管资源进行释放,
//如果是析构函数调用时由于GC已经执行,所以不能保证托管资源是有已经被释放,所以最好不要对托管资源进行释放
//调用托管资源的Dispose方法释放其资源(托管&非托管)
}
//释放非托管资源(断开数据库连接,关闭流等)
}
isDisponsed = true;
}
~MyClass
{
Dispose(false);
}
//一般该类的所有方法都应包含对isDispose的判断
public void SomeMethod()
{
if(isDispose)
{
throw new ObjectDisposedException();
}
//方法的具体实现
}
}