来到个新地方,新学习C#,前面看到C#的垃圾回收,Finalize和Dispose时,总是一知半解,迷迷糊糊。这次好了,前面连续两次面试问到这个问题,脑子里不是很清晰,加上用英文来表达,更是雪上加霜的感觉。
回来,好好看了相关资料,从网上看,总没有人能说的很清晰,往往很深奥的样子,拿了本<C# language 7.0>,这样中英文结合看,总算清晰了。
现在主要是找工作,没有时间写详细,先就说个重点:
1. 垃圾回收器GC负责托管对象的回收。GC通过从应用根对象开始(如全局变量,静态变量等)去访问对象到来判断对象是否能回收,如果无法到达对象,则对像可以被回收。
而不是通过引用计数法来判断对象是否需要被回收,因为引用计数无法解决一个循环引用的两个对象的回收,他们的引用计数都为1,但实际这两个对象是整个应用程序中的孤岛对象,
从应用程序根对象开始追踪是无法访问到的,应该被回收。(后续详细解释)
2. 垃圾回收很耗性能,一般只在分配新对象发现空间不够用时才触发回收。也就是说,何时回收对用户来说是不可控,未知的。
具体回收时采用的算法是分步回收,即分代回收的方法。其基于统计原理,新产生的对象总是有最大的可能性不被使用而需要回收,比如新调用函数中的新分配的局部对象;而经过几次回收周期中存活下来的对象,则更不可能被回收掉,如全局变量等。
GC初始化默认每个对象都是需要回收的,回收触发时,通过检测从根对象开始是否能访问到来判断对象是否真的可以回收。GC将对象标签为3代,分别是0,1,和2代对象。0代是还没有经过一次回收检测的对象(没检测过,默认都是可以回收的),1代表示经过一次回收检测后存活下来(即不能回收)的对象,而2代表示经过2次以上检测仍存活的对象。
GC检测依照顺序,分别检测0代,1代和2代。但是如果检测到0代,回收部分对象后,发现空间已经足够新对象使用,回收检测立刻停止。如果仍然不够,才会继续检测1代对象,直至2代对象。通过这样的方法,避免垃圾回收机制产生作用时的性能消耗。大部分情况下,0代检测就能释放足够空间应付新对象的分配,这样后续的垃圾回收过程就可以避免。
从回收过程也可以看出,一次回收过程中,哪个对象能被回收,也是不可控,未知的。
3.垃圾回收器对堆托管对象可以回收,但对非托管对象就需要使用用户手动回收。非托管对象包括文件句柄,socket,数据库连接等。这类对象除了本身是托管外,其内部还包含对操作系统资源申请的对象,这类对象需要使用者主动释放。如文件打开后,需要关闭。
非托管对象的释放,C#提供了两种方式。一种是使用Finalize(),一种是使用Dispose().
3.1 Finalize接口是Object的虚保护函数,C#所有的对象都继承自Object,所以都天然可以重写该接口。但C#编译器在此处做了限制,外部用户只能改写析构函数,编译器编译析构函数时,自动会加上Finalize()的调用。因此加上析构函数,就等于重写了Finalize接口。析构函数是垃圾回收器负责调用的,那就意味着Finalize的调用,也是GC负责调用。考虑到上面GC回收对象的机制的特点,我们可以得出Finalize是不确定何时会调用的,虽然肯定的是,其最后始终会被调用。
Finalize被GC调用的具体过程是:新对象产生时,CLR会监测到Finalize有重写,会将对象建立一个指针引用放入一个Finalization Queue;GC发生作用时,如果该对象被检测出可以回收,则会先将其从Finalization Queue的引用队列移到另外一个叫freachable的引用队列,等待下个回收周期时,在对象空间被释放前,该队列的里对象的Finalize接口被保证调用。因此Finalize比较耗时,至少要两个回收周期才能回收。
Dispose()来自于接口IDispose,所以只要继承IDispose接口,我们就可以重写Dispose,在里面来释放非托管对象。与Finalize不同的是,Dispose接口的调用,需要使用者自己确定何时调用,因此该调用是明确见效的。使用具有Dispose接口的对象时,一般是采用catch,finally 的形式,在finally调用dispose,这样确保不管出现什么情况dispose可以被调用到。C#利用using关键字简化了该种调用方式。(using db = new DbMyContext()){ ... }这样离开这个using语句块后,CLI会自动调用using新生成对象的Dispose接口。
3.2 这两者方式各有优缺点。Finalize可以由GC确保最终会调用,非托管对象可以被释放,用户可以不用操心,但又不确定何时会释放。Dispose可以明确立刻释放,但又是不可靠的,因为使用者(程序员自己)可能会忘记显式的调用Dispose接口。
基于此,最好的办法是利用两者的优点,一起使用。就是说在这两个地方都来释放,确保非托管资源一定释放。因为Fialize的调用是垃圾回收器处理,消耗性能和时间。一旦确定已经调用了Dispose以后,后续的Finalize是可以不用调用的,GC.SuppressFinalize(this)可以完成该功能来,停止Finalize的使用。
基于此,微软给出了一种推荐的写法:
public class MyRefObj : IDispose{ private bool disposed = false; // used to identify whether the Dispose() has been called //disposing is used to indicate whethet to explicitly call the managed objects' dispose that you expect to manage intentionally void CleanUp(bool disposing){ if (!this.disposed) { if (disposing){ ... // Clean up the managed resource explicitly, like call the managed objects' Dispose } ... // Clean up the unmanaged resource } disposed = true; } public void Dispose() { CleanUp(true); // Now suppress finalization. GC.SuppressFinalize(this); } ~ MyRefObj() { CleanUp(false); } }
说明,析构函数中的CleanUP参数不能为true,因为CleanUp里面会对一些托管资源调用Dispose接口,而托管资源何时被回收是不确定的,因此这些调用的行为是不确定的,所以不能调用。
基本要点都说完了,后面在补充细节说明。
1. C#中的Object,Class和reference的关系
Object是Class的一个实例,而Reference是一个指针。学过C/C++的很好理解,实际其内容是一个内存地址,该内存地址里存放的是实际的对象。
C#中所有的类的实例化都是通过引用实现,实际是使用new 来实例化一个类。如下举例说明:
class Program { static void Main(string[] args) { Console.WriteLine("***** GC Basics *****"); // Create a new Car object on the managed heap. We are returned a reference to this object ("refToMyCar"). Car refToMyCar = new Car("Zippy", 50); // The C# dot operator (.) is used to invoke members on the object using our reference variable. Console.WriteLine(refToMyCar.ToString()); Console.ReadLine(); } }
其中变量refToMyCar实际就是一个引用,一个指针,指向实际的实例对象Car("Zippy", 50)
在C#中,引用refToMyCar和实例对象Car("Zippy", 50)是存放于不同的内存块中,分别叫做堆栈区和托管内存堆区;
程序的堆栈区是一个临时数据区,存放函数调用的对象,局部变量等,函数调用完存放于堆栈区的对象的生命期就结束了,每个线程都有固定大小的堆栈。
而堆是一个全局内存区,所有分配在Managed Heap的对象,需要管理何时释放。在C/C++语言中,这部分数据需要显示的delete和new配套使用,而在C#中,会被.NET CLR来管理,就是说你在托管堆中只管生成新的实例,而不用管该实例占用空间的释放,这由CLR自动管理,实际就是垃圾回收器干的事情。
2. 垃圾回收器(GC)
GC是怎样知道一个对象不在使用了呢,这涉及到后面描述的具体算法,简而言之就是GC发现,无法从代码根应用到达的对象。可以以一个不太严谨的解释来理解,就是该分配在堆区的
对象,已经没有外部引用了。如下一段代码:
static void MakeACar() { // If myCar is the only reference to the Car object, it *may* be destroyed when this method returns. Car myCar = new Car(); }
Car的对象在函数外,没有可以引用到,就意味着该对象可以被回收了。
3.
C#有两类对象,托管和非托管对象。这个托管指的是对象被谁管理,在.net框架里,就是被CLR托管。非托管对象,指的是一些操作系统提供的资源,比如文件句柄,Socket,数据库连接等。
这些资源对象的使用,需要像操作系统申请并且使用完后,及时的归还。比如C#的FileStream类,我们使用时有如下一段代码:
FileInfo finfo = new FileInfo(FilePath); //以打开或者写入的形式创建文件流 using (FileStream fs = finfo.OpenWrite()) ...{ //根据上面创建的文件流创建写数据流 StreamWriter sw = new StreamWriter(fs, System.Text.Encoding.Default); //把新的内容写到创建的HTML页面中 sw.WriteLine(strhtml); sw.Flush(); sw.Close(); }
其中,finfo, fs, sw三个分别为FileInfo,FileStream,StreamWriter的对象。