第一个就是很多人用 .Net 写程序,会谈到托管这个概念。那么 .Net 所指的资源托管到底是什么意思,是相对于所有资源,还是只限于某一方面资源?很多人对此不是很了解,其实 .Net 所指的托管只是针对内存这一个方面,并不是对于所有的资源;因此对于 Stream ,数据库的连接, GDI+ 的相关对象,还有 Com 对象等等,这些资源并不是受到 .Net 管理而统称为非托管资源。而对于内存的释放和回收,系统提供了 GC-Garbage Collector ,而至于其他资源则需要手动进行释放。
那么第二个概念就是什么是垃圾,通过我以前的文章,会了解到 .Net 类型分为两大类,一个就是值类型,另一个就是引用类型。前者是分配在栈上,并不需要 GC 回收;后者是分配在堆上,因此它的内存释放和回收需要通过 GC 来完成。 GC 的全称为“ Garbage Collector ” , 顾名思义就是垃圾回收器,那么只有被称为垃圾的对象才能被 GC 回收。也就是说,一个引用类型对象所占用的内存需要被 GC 回收,需要先成为垃圾。那么 .Net 如何判定一个引用类型对象是垃圾呢, .Net 的判断很简单,只要判定此对象或者其包含的子对象没有任何引用是有效的,那么系统就认为它是垃圾。
明确了这两个基本概念,接下来说说 GC 的运作方式以及其的功能。内存的释放和回收需要伴随着程序的运行,因此系统为 GC 安排了独立的线程。那么 GC 的工作大致是,查询内存中对象是否成为垃圾,然后对垃圾进行释放和回收。那么对于 GC 对于内存回收采取了一定的优先算法进行轮循回收内存资源。其次,对于内存中的垃圾分为两种,一种是需要调用对象的析构函数,另一种是不需要调用的。 GC 对于前者的回收需要通过两步完成,第一步是调用对象的析构函数,第二步是回收内存,但是要注意这两步不是在 GC 一次轮循完成,即需要两次轮循;相对于后者,则只是回收内存而已。
很明显得知,对于某个具体的资源,无法确切知道,对象析构函数什么时候被调用,以及 GC 什么时候会去释放和回收它所占用的内存。那么对于从 C 、 C++ 之类语言转换过来的程序员来说,这里需要转变观念。
那么对于程序资源来说,我们应该做些什么,以及如何去做,才能使程序效率最高,同时占用资源能尽快的释放。前面也说了,资源分为两种,托管的内存资源,这是不需要我们操心的,系统已经为我们进行管理了;那么对于非托管的资源,这里再重申一下,就是 Stream ,数据库的连接, GDI+ 的相关对象,还有 Com 对象等等这些资源,需要我们手动去释放。
如何去释放,应该把这些操作放到哪里比较好呢。 .Net 提供了三种方法,也是最常见的三种,大致如下:
1. 析构函数;
2. 继承 IDisposable 接口,实现 Dispose 方法;
3. 提供 Close 方法。
经过前面的介绍,可以知道析构函数只能被 GC 来调用的,那么无法确定它什么时候被调用,因此用它作为资源的释放并不是很合理,因为资源释放不及时;但是为了防止资源泄漏,毕竟它会被 GC 调用,因此析构函数可以作为一个补救方法。而 Close 与 Dispose 这两种方法的区别在于,调用完了对象的 Close 方法后,此对象有可能被重新进行使用;而 Dispose 方法来说,此对象所占有的资源需要被标记为无用了,也就是此对象被销毁了,不能再被使用。 例如,常见 SqlConnection 这个类,当调用完 Close 方法后,可以通过 Open 重新打开数据库连接,当彻底不用这个对象了就可以调用 Dispose 方法来标记此对象无用,等待 GC 回收。明白了这两种方法的意思后,大家在往自己的类中添加的接口时候,不要歪曲了这两者意思。
接下来说说这三个函数的调用时机,我用几个试验结果来进行说明,可能会使大家的印象更深。
首先是这三种方法的实现,大致如下:
/// <summary>
/// The class to show three disposal function
/// </summary>
public class DisposeClass:IDisposable
{
public void Close()
{
Debug.WriteLine( "Close called!" );
}
~DisposeClass()
{
Debug.WriteLine( "Destructor called!" );
}
#region IDisposable Members
public void Dispose()
{
// TODO: Add DisposeClass.Dispose implementation
Debug.WriteLine( "Dispose called!" );
}
#endregion
}
对于 Close 来说不属于真正意义上的释放,除了注意它需要显示被调用外,我在此对它不多说了。而对于析构函数而言,不是在对象离开作用域后立刻被执行,只有在关闭进程或者调用 GC.Collect 方法的时候才被调用,参看如下的代码运行结果。
private void Create()
{
DisposeClass myClass = new DisposeClass();
}
private void CallGC()
{
GC.Collect();
}
// Show destructor
Create();
Debug.WriteLine( "After created!" );
CallGC();
运行的结果为:
After created!
Destructor called!
显然在出了 Create 函数外, myClass 对象的析构函数没有被立刻调用,而是等显示调用 GC.Collect 才被调用。
对于 Dispose 来说,也需要显示的调用,但是对于继承了 IDisposable 的类型对象可以使用 using 这个关键字,这样对象的 Dispose 方法在出了 using 范围后会被自动调用。例如:
using ( DisposeClass myClass = new DisposeClass() )
{
//other operation here
}
如上运行的结果如下:
Dispose called!
那么对于如上 DisposeClass 类型的 Dispose 实现来说,事实上 GC 还需要调用对象的析构函数,按照前面的 GC 流程来说, GC 对于需要调用析构函数的对象来说,至少经过两个步骤,即首先调用对象的析构函数,其次回收内存。也就是说,按照上面所写的 Dispose 函数,虽说被执行了,但是 GC 还是需要执行析构函数,那么一个完整的 Dispose 函数,应该通过调用 GC.SuppressFinalize(this ) 来告诉 GC ,让它不用再调用对象的析构函数中。 那么改写后的 DisposeClass 如下:
/// <summary>
/// The class to show three disposal function
/// </summary>
public class DisposeClass:IDisposable
{
public void Close()
{
Debug.WriteLine( "Close called!" );
}
~DisposeClass()
{
Debug.WriteLine( "Destructor called!" );
}
#region IDisposable Members
public void Dispose()
{
// TODO: Add DisposeClass.Dispose implementation
Debug.WriteLine( "Dispose called!" );
GC.SuppressFinalize( this );
}
#endregion
}
通过如下的代码进行测试。
private void Run()
{
using ( DisposeClass myClass = new DisposeClass() )
{
//other operation here
}
}
private void CallGC()
{
GC.Collect();
}
// Show destructor
Run();
Debug.WriteLine( "After Run!" );
CallGC();
运行的结果如下:
Dispose called!
After Run!
显然对象的析构函数没有被调用。通过如上的实验以及文字说明,大家会得到如下的一个对比表格。
| 析构函数 | Dispose 方法 | Close 方法 |
意义 | 销毁对象 | 销毁对象 | 关闭对象资源 |
调用方式 | 不能被显示调用,会被 GC 调用 | 需要显示调用 或者通过 using 语句 | 需要显示调用 |
调用时机 | 不确定 | 确定,在显示调用或者离开 using 程序块 | 确定,在显示调用时 |
那么在定义一个类型的时候,是否一定要给出这三个函数地实现呢。
我的建议大致如下。
1. 提供析构函数,避免资源未被释放,主要是指非内存资源;
2. 对于 Dispose 和 Close 方法来说,需要看所定义的类型所使用的资源(参看前面所说),而决定是否去定义这两个函数;
3. 在实现 Dispose 方法的时候,一定要加上“ GC.SuppressFinalize( this ) ”语句,避免再让 GC 调用对象的析构函数。
C# 程序所使用的内存是受托管的,但不意味着滥用,好地编程习惯有利于提高代码的质量以及程序的运行效率。