C#中Dispose,finalize,GC,析构函数区别
(文章写得比较清楚和详细了,特转载过来备忘)
(原始出处暂未找到-感谢作者的分享)
释放类所使用的未托管资源的两种方式:
1.利用运行库强制执行的析构函数,但析构函数的执行是不确定的,而且,由于垃圾收集器的工作方式,它会给运行库增加不可接受的系统开销。
2.IDisposable接口提供了一种机制,允许类的用户控制释放资源的时间,但需要确保执行Dispose()。
一般情况下,最好的方法是执行这两种机制,获得这两种机制的优点,克服其缺点。假定大多数程序员都能正确调用Dispose(),实现IDisposable接口,同时把析构函数作为一种安全的机制,以防没有调用Dispose()。
Dispose()有第二个protected重载方法,它带一个bool参数,这是真正完成清理工作的方法。Dispose(bool)由析构函数和IDisposable.Dispose()调用。这个方式的重点是确保所有的清理代码都放在一个地方。
传递给Dispose(bool)的参数表示Dispose(bool)是由析构函数调用,还是由IDisposable.Dispose()调用——Dispose(bool)不应从代码的其他地方调用,其原因是:
● 如果客户调用IDisposable.Dispose(),该客户就指定应清理所有与该对象相关的资源,包括托管和非托管的资源。
● 如果调用了析构函数,在原则上,所有的资源仍需要清理。但是在这种情况下,析构函数必须由垃圾收集器调用,而且不应访问其他托管的对象,因为我们不再能确定它们的状态了。在这种情况下,最好清理已知的未托管资源,希望引用的托管对象还有析构函数,执行自己的清理过程。
isDispose成员变量表示对象是否已被删除,并允许确保不多次删除成员变量。这个简单的方法不是线程安全的,需要调用者确保在同一时刻只有一个线程调用方法。要求客户进行同步是一个合理的假定,在整个.NET类库中反复使用了这个假定(例如在集合类中)。
最后,IDisposable.Dispose()包含一个对System.GC. SuppressFinalize()方法的调用。SuppressFinalize()方法则告诉垃圾收集器有一个类不再需要调用其析构函数了。因为Dispose()已经完成了所有需要的清理工作,所以析构函数不需要做任何工作。调用SuppressFinalize()就意味着垃圾收集器认为这个对象根本没有析构函数
- <span style="font-size:14px;">public class ResourceHolder : IDisposable
- {
- private bool isDispose = false;
- public void Dispose()
- {
- Dispose(true);
- GC.SuppressFinalize(this);
- }
- protected virtual void Dispose(bool disposing)
- {
- if (!isDisposed)
- {
- if (disposing)
- {
- //释放托管资源
- }
- //释放非托管资源
- }
- isDisposed=true;
- }
- ~ResourceHolder()
- {
- Dispose(false);
- }
- } </span>
1.被分配内存空间的对象最有可能被释放。在方法执行时,就需要为该方法的对象分配内存空间,搜索最近分配的对象集合有助于花费最少的代价来尽可能多地释放内存空间。
2.生命期最长的对象释放的可能性最小,经过几轮垃圾回收后,对象仍然存在,搜索它时就需要进行大量的工作,却只能释放很小的一部分空间。
3.同时被分配内存的对象通常是同时使用,将它们彼此相连有助于提高缓存性能和回收效率。
C#中的回收器是分代的垃圾回收器(Gererational Garbage Collector) 它将分配的对象分为3个类别或代。(可用GC.GetGeneration方法返回任意作为参数的对象当前所处的代)最近被分配内存的对象被放置于第0 代,因为第0代很小,小到足以放进处理器的二级(L2)缓存,所以它能够提供对对象的快速存取。经过一轮垃圾回收后,仍然保留在第0代中的对象被移进第1 代中,再经过一轮垃圾内存回收后,仍然保留在第1代中的对象则被移进第2代中,第2代中包含了生存期较长的对象。(类比于Java中的分代收集器)
在C#中值类型是在堆栈中分配内存,它们有自身的生命周期,所以不用对它们进行管理,会自动分配和释放。而引用类型是在堆中分配内存的。所以它的分配和释放就需要像回收机制来管理。C#为一个对象分配内存时,托管堆可以立即返回新对象所需的内存,因为托管堆类似于简单的字节数组,有一个指向第一个可用内存空间的指针,指针像游标一样向后移动,一段段内存就分配给了正在运行的程序的对象。在不需要太多垃圾回收的程序中,托管堆性能优于传统的堆。
当第0代中没有可以分配的有效内存时,就触发了第0代中的一轮垃圾回收,它将删除那些不再被引用的对象,并将当前正在使用的对象移至第1代。而当第0代垃圾回收后依然不能请求到充足的内存时,就启动第1代垃圾回收。如果对各代都进行了垃圾回收后仍没有可用的内存就会引发一个 OutOfMemoryException异常。
终结器(Finalize方法)
在有些情况下,类可以提供一个终结器在对象被销毁时执行,终结器是一个名为Finalize的受保护的方法:
protected void Finalize()
{
base.Finalize();
//释放外部资源
}
垃圾回收器使用名为“终止队列”的内部结构跟踪具有 Finalize 方法的对象。每次您的应用程序创建具有 Finalize 方法的对象时,垃圾回收器都在终止队列中放置一个指向该对象的项。托管堆中所有需要在垃圾回收器回收其内存之前调用它们的终止代码的对象都在终止队列中含有项。(实现Finalize方法或析构函数对性能可能会有负面影响,因此应避免不必要地使用它们。用Finalize方法回收对象使用的内存需要至少两次垃圾回收。当垃圾回收器执行回收时,它只回收没有终结器的不可访问对象的内存。这时,它不能回收具有终结器的不可访问对象。它改为将这些对象的项从终止队列中移除并将它们放置在标为准备终止的对象列表中。该列表中的项指向托管堆中准备被调用其终止代码的对象。垃圾回收器为此列表中的对象调用Finalize方法,然后将这些项从列表中移除。后来的垃圾回收将确定终止的对象确实是垃圾,因为标为准备终止对象的列表中的项不再指向它们。在后来的垃圾回收中,实际上回收了对象的内存。概括而言,就是将垃圾回收分为了三个阶段,第一个阶段回收没有Finalize方法或者析构函数的对象,第二个阶段调用那些析构函数或者Finalize方法,第三个阶段回收那些刚调用了析构函数或者Finalize方法的对象。)
终结器(finalizer)是在回收过程中,由垃圾回收器调用的方法。如何含有终结器的对象到了G2中,那么就会需要非常长的时间来回收。事实上,根据应用程序运行时间的长短,对象很有机会直到应用程序退出之前都不会被回收(特别是其中包含的重要的资源得不得释放,将会对性能产生很大的影响,比如说数据库连接得不到释放。)
Dispose方法
在不使用终结器时,可以考虑使用Dispose方法,你可以使用这个方法来释放所保存包括的在托管对象引用在内的任何资源。系统类中如何实现了Dispose方法,那么一般Dispose方法中都包含了SuppressFinalize方法,这个方法会告知系统这个类已经不再需要析构了,这样可以提高释放资源的效率。所以在自定义类中的Dispose方法应该调用GC.SuppressFinalize来告知运行时这些对象不需要析构。如下所示:
public void Dispose(){
object.Dispose();
dbConnection.Dispose();
GC.SuppressFinalize(this);//申明不需要终结
}
创建并使用了Dispose方法的对象,就需要使用完该对象之后调用这些方法,最好是在Finally中调用。
System.GC类
GC类包含了可使用户与垃圾回收机制进行互操作的静态方法,包括发起新一轮垃圾回收操作的方法。确定某对象当前所在代的方法及当前分配内存空间的方法。
GC.Collect();//无参时将发起一轮全面的回收。(完全回收之前,应用程序会停止响应,因此不建议使用。)
GC.Collect(i);//(0<=i<=2)对第i代进行垃圾回收。
GetTotalMemory将返因分配于托管堆上的内存空间总量。当参数为True时,在计算之前将进行一轮全面的垃圾回收。如下所示:
long totalMemory = System.GC.GetTotalMemory(True);
下面是 在.NET Framework 2.0 版中是新增的公共方法:
通知运行库在安排垃圾回收时应考虑分配大量的非托管内存
public static void AddMemoryPressure (long bytesAllocated)//bytesAllocated已分配的非托管内存的增量。
返回已经对对象的指定代进行的垃圾回收次数。
public static int CollectionCount (int generation)
通知运行库已释放非托管内存,在安排垃圾回收时不需要再考虑它。
public static void RemoveMemoryPressure (long bytesAllocated)
C# 中的析构函数实际上是重写了 System.Object 中的虚方法 Finalize
三种最常的方法如下:
1. 析构函数;(由GC调用,不确定什么时候会调用)
2. 继承IDisposable接口,实现Dispose方法;(可以手动调用。比如数据库的连接,SqlConnection.Dispose(),因为如果及时释放会影响数据库性能。这时候会用到这个,再如:文件的打开,如果不释放会影响其它操作,如删除操作。调用Dispose后这个对象就不能再用了,就等着被GC回收。)
3. 提供Close方法。(类似Dispose但是,当调用完Close方法后,可以通过Open重新打开)
析构函数不能显示调用,而对于后两种方法来说,都需要进行显示调用才能被执行。而Close与Dispose这两种方法的区别在于,调用完了对象的Close方法后,此对象有可能被重新进行使用;而Dispose方法来说,此对象所占有的资源需要被标记为无用了,也就是此对象要被销毁,不能再被使用。
|
析构函数 |
Dispose方法 |
Close方法 |
意义 |
销毁对象 |
销毁对象 |
关闭对象资源 |
调用方式 |
不能被显示调用,在GC回收是被调用 |
需要显示调用 或者通过using语句 |
需要显示调用 |
调用时机 |
不确定 |
确定,在显示调用或者离开using程序块 |
确定,在显示调用时 |
下面提供一个模式来结合上面的 析构函数和Dispose方法。
- <span style="font-size:14px;">public class BaseResource: IDisposable
- { //前面我们说了析构函数实际上是重写了 System.Object 中的虚方法 Finalize, 默认情况下,一个类是没有析构函数的,也就是说,对象被垃圾回收时不会被调用Finalize方法
- ~BaseResource()
- { // 为了保持代码的可读性性和可维护性,千万不要在这里写释放非托管资源的代码
- // 必须以Dispose(false)方式调用,以false告诉Dispose(bool disposing)函数是从垃圾回收器在调用Finalize时调用的
- Dispose(false);
- }
- // 无法被客户直接调用
- // 如果 disposing 是 true, 那么这个方法是被客户直接调用的,那么托管的,和非托管的资源都可以释放
- // 如果 disposing 是 false, 那么函数是从垃圾回收器在调用Finalize时调用的,此时不应当引用其他托管对象所以,只能释放非托管资源
- protected virtual void Dispose(bool disposing)
- {
- // 那么这个方法是被客户直接调用的,那么托管的,和非托管的资源都可以释放
- if(disposing)
- {
- // 释放 托管资源
- OtherManagedObject.Dispose();
- }
- //释放非托管资源
- DoUnManagedObjectDispose();
- // 那么这个方法是被客户直接调用的,告诉垃圾回收器从Finalization队列中清除自己,从而阻止垃圾回收器调用Finalize方法.
- if(disposing)
- GC.SuppressFinalize(this);
- }
- //可以被客户直接调用
- public void Dispose()
- {
- //必须以Dispose(true)方式调用,以true告诉Dispose(bool disposing)函数是被客户直接调用的
- Dispose(true);
- }
- } </span>
上面的范例达到的目的:
1/ 如果客户没有调用Dispose(),未能及时释放托管和非托管资源,那么在垃圾回收时,还有机会执行Finalize(),释放非托管资源,但是造成了非托管资源的未及时释放的空闲浪费
2/ 如果客户调用了Dispose(),就能及时释放了托管和非托管资源,那么该对象被垃圾回收时,不会执行Finalize(),提高了非托管资源的使用效率并提升了系统性能
最后:
如果类中使用了非托管资源,则要考虑提供Close方法,和Open方法。并在您的Dispose方法中先调用 Close方法。
在使用已经有类时,如SqlConnection。如果暂时不用这个连接,可以考虑用Close()方法。如果不用了就考虑调用Dispose()方法。