·天新网首页·加入收藏·设为首页·网站导航
数码笔记本手机摄像机相机MP3MP4GPS
硬件台式机网络服务器主板CPU硬盘显卡
办公投影打印传真
家电电视影院空调
游戏网游单机动漫
汽车新车购车试驾
下载驱动源码
学院开发设计
考试公务员高考考研
业界互联网通信探索
您现在的位置:天新网 > 软件开发 > .Net开发 > C#
C#基础知识梳理系列十二:终结操作及资源清理
http://www.21tx.com 2012年08月28日 博客园 solan3000

1 2 下一页

第一节 析构函数和Finalize方法

C#C++有着类似的析构函数,都是对资源进行清理,但是,在C++中,开发人员明确知道析构函数会被调用,而C#中,开发人员不太明确析构函数会在什么时候被调用,它是由CLR管理的,通常是在一个对象被标记为垃圾对象,如果有析构函数,CLR的垃圾回收器会先调用析构函数,然后再回收其内存

类型System.Object有一个受保护的虚方法protected virtual void Finalize();这个就是“析构函数”。如果想为一个类型添加析构函数,必须使用与C++类型的语法结构:前置波浪线+类名,相当于无参构造函数的名前加上波浪线,如下:

public class Code_12 : IApp 
    { 
        public void DoWork() 
        { 
   
        } 
        ~Code_12() 
        { 
            Console.WriteLine("Clear Code_12"); 
        } 
    }

析构函数前不能有任何访问修饰符,并且一个类型只能有一个析构函数。编译后,上面的~Code_12()被编译成名为Finalize的方法,如下图:

C#基础知识梳理系列十二:终结操作及资源清理

可以看到,编译过程实际上是对基类Object的虚方法Finalize()的重写,可以非常强悍地认为Finalize就是析构函数~Code_12()的别名,二者只是书写方式不同,干的都是一样的擦屁股的活。我们再来看一下它的内部IL:

.method family hidebysig virtual instance void
        Finalize() cil managed 
{ 
  // 代码大小       25 (0x19) 
  .maxstack  1 
  .try
  { 
    IL_0000:  nop 
    IL_0001:  ldstr      "Clear Code_12"
    IL_0006:  call       void [mscorlib]System.Console::WriteLine(string) 
    IL_000b:  nop 
    IL_000c:  nop 
    IL_000d:  leave.s    IL_0017 
  }  // end .try 
  finally
  { 
    IL_000f:  ldarg.0 
    IL_0010:  call       instance void [mscorlib]System.Object::Finalize() 
    IL_0015:  nop 
    IL_0016:  endfinally 
  }  // end handler 
  IL_0017:  nop 
  IL_0018:  ret 
} // end of method Code_12::Finalize

可以看到,Finalize()方法内实际上是将代码包装到try…finally块内,我们实现的代码被放到了try块,在finally块内调用了基类的Finalize方法,相当于base.Finalize()。

前面我们说过,析构函数是在垃圾回收器回收垃圾对象之前的最后才执行一些清理工作,它的执行是受CLR管理,非人工可控,我们通过一个示例代码来看一下它的执行顺序:

public class Code_12_01 
    { 
        public Code_12_01() 
        { 
            Console.WriteLine("Create Code_12_01"); 
        } 
        ~Code_12_01() 
        { 
            Console.WriteLine("Clear Code_12_01"); 
        } 
    } 
    public class Code_12_02 : Code_12_01 
    { 
        public Code_12_02() 
        { 
            Console.WriteLine("Create Code_12_02"); 
        } 
        ~Code_12_02() 
        { 
            Console.WriteLine("Clear Code_12_02"); 
        } 
    } 
    public class Code_12_03 : Code_12_02 
    { 
        public Code_12_03() 
        { 
            Console.WriteLine("Create Code_12_03"); 
        } 
        ~Code_12_03() 
        { 
            Console.WriteLine("Clear Code_12_03"); 
        } 
    }

执行以下代码,先创建,再遗弃,最后垃圾回收:

public void DoWork() 
{ 
    Code_12_03 temp = new Code_12_03(); 
    temp = null; //遗弃对象,等待垃圾回收 
    GC.Collect(); 
}

打印结果:

Create Code_12_01 
Create Code_12_02 
Create Code_12_03 
Clear Code_12_03 
Clear Code_12_02 
Clear Code_12_01

在调用析构函数过程中,是从派生类逐级向上调用基类的析构函数。

CLR对这种在垃圾回收前调用对象的析构函数进行资源清理的工作就是终结操作。我们不能控制垃圾回收,同样也不能控制终结操作,调用GC.Collect();只是向CLR发出垃圾回收的请求,垃圾回收器对一第列对象回收的先后顺序,我们是无法控制的。

第二节 终结操作

在我们平时开发工作中,或多或少都会用到本地资源,如打开文件,网络连接等,对这些资源的操作实际上是CLR通过Windows获取资源的独占句柄,使用完后,必须释放句柄,否则其他访问者将无法使用,例如我们经常碰到的异常“文件XXX正常由另一进程使用,因此该进程无法访问该文件。”就是这种原因造成的。

终结操作就是利用析构函数来释放/清理资源,在对象被垃圾回收前,CLR调用Finalize()方法做清理工作,前提是我们提供了析构函数。这通常在我们定义类型中使用到本地资源的时候非常有用。如下一个文件管理类:

public class FileManager 
    { 
        FileStream fs = null; 
        public FileManager() 
        { 
   
        } 
        ~FileManager() 
        { 
            if (fs != null) 
            { 
                fs.Close(); 
            } 
        } 
    }

在调用析构函数中,应该确保其内部不该出异常,其实前一节中~Code_12()的IL代码也可以看出来,并没有与try对应的catch块。所以我们在~FileManager()内对fs进行了判空。垃圾回收器发现FileManager对象不再可用时,会调用Finalize(),在内部关闭fs,接着就是回收其内存了。通常在有垃圾回收的时候都有可能调用Finalize()方法。

事实上,在实现了Finalize()的对象内存被回收过程并不是如此简单,这个终结操作有时可能须要执行两次或更多次垃圾回收才能达到释放其内存的目的,继续往下看。

先来看两个垃圾回收器管理的列表:

终结列表(Finalization List):放置所有实现了Finalize()方法的对象的指针。

Frachable队列:放置已被认定为垃圾对象且实现了Finalize()的对象的指针,这里的指针是从终结列表中移过来的。

如果类型实现了Finalize(),在创建该类型对象前,即调用构造器之前,CLR会将该对象的一个指针放到一个终结列表(Finalization List)中,终结列表是由垃圾回收器管理的一个数据结构。在一次垃圾回收前,垃圾回收器会标记所有的不可达对象,这时所有的不可达对象已经被判了“死刑”,接着扫描终结列表以查找实现了Finalize()方法的对象的指针,并将这些指针从终结列表中移到Frachable队列中,当Frachable队列不为空时,CLR有一个专门的线程来负责调用队列中指针对应对象的Finalize方法,为了能够调用这些对象的Finalize()方法,必须重新激活这些对象,也就是从“不可达”状态变成“可达”状态,这个过程是使对象复活的过程,调用完Finalize()方法后,该对象就彻底完蛋了,接着就是等待下一次垃圾回收时对其内存进行回收,它永远再无出头之日了(当然,如果在析构函数中把该对象的指针放到一个其他静态变量中,那情况就不一样了。有兴趣的可以自己测试一下。)。在这个过程中,假如对象已被标记为垃圾,但未调用其Finalize方法前,该对象可能被从第0代提升为第1代,则有可能要经过更多次垃圾回收才能释放其内存。

GC类提供了一个静态方法GC.WaitForPendingFinalizers(), 此方法将挂起当前线程,接着垃圾回收器清空Frachable队列并调用其中对象的Finalize()方法,全部调用完毕后被挂起的线程才得以恢复。前面的描述中我们知道,即使调用了Finalize()方法也不一定能立即释放该对象的内存,所以可以在GC.WaitForPendingFinalizers()方法后立即跟一个GC.Collect()进行回收,但微软不建议我们不这么干。

上一篇: C#基础知识梳理系列九:StringBuilder
下一篇: C# 温故而知新:Stream篇(—)

1 2 下一页

关于我们 | 联系我们 | 加入我们 | 广告服务 | 投诉意见 | 网站导航
Copyright © 2000-2011 21tx.com, All Rights Reserved.
晨新科技 版权所有 Created by TXSite.net