定义了一个类后,我们就可以new任何数量的对象。此时,将产生托管堆和栈上的内存分配,堆上将开辟一块新的空间负责储存类对象,而栈上仅仅储存引用。一般来说,垃圾收集和内存管理仅仅是相对于托管堆而言的。而c#的内存管理非常方便-就是根本不用管理,垃圾收集器将负责所有的工作。对于垃圾收集,有几个问题需要明确。
什么是垃圾:通常来说,某个对象如果在代码的任何部分都不可访问的时候,就将其视为垃圾。
垃圾收集器何时工作:当它认为需要的时候(下文详解)或者用户呼叫。
垃圾收集器不会理会非托管资源,那么怎么办:自己写代码释放,有finalize和dispose两种方法
基本概念
托管堆保存着一个指针,其指示着下一个对象将被分配的位置,当系统运行到new关键字时,将会在CIL中加入newobj指令,其负责下面的工作:
1. 计算要分配的新对象所需要的总内存数,包括所有自身成员和基类
2. 检查托管堆上的指针,判断托管堆是否还有足够的内存分配该对象,如果足够则开始执行静态和普通构造函数,然后返回引用(指向下一个对象的指针的上一个位置)
3. 移动托管堆指针到下一个可用的位置
4. 如果第二步发现空间不够,则执行一次垃圾回收
注意,将对象设为null并不会引起垃圾回收,或者清除托管堆上的空间。这仅仅是将对象和托管堆两者的联系(引用)移除,即对象不指向托管堆上的任何东西。
基本算法
当进行垃圾回收时,运行库将检查托管堆中的对象,判断应用程序是否仍然可以访问他们。整个垃圾回收分为两步。
阶段1: Mark-Sweep 标记清除阶段,先假设heap中所有对象都可以回收,然后找出不能回收的对象,给这些对象打上标记,最后heap中没有打标记的对象都是可以被回收的
垃圾回收器在执行回收时通过检查应用程序的根来确定不再使用的对象。简单的说,根就是一个存储位置,保存着对托管堆上一个对象的引用(就是上图中的那两个箭头)。每个应用程序都有一组根,它们被储存在一个列表中,每个根或者引用托管堆中的对象,或者设置为null。
垃圾回收器可以访问活动根的列表,对照此列表检查应用程序的根,并在此过程中创建一个图表,在其中包含所有可从这些根中访问的对象。不在该图表中的对象将无法从应用程序的根中访问。垃圾回收器会考虑将无法访问的对象标记为垃圾,并释放为它们分配的内存。在下图中,由于将c2设置为Null,没有任何根指向托管堆中的c2,于是其将被视为垃圾。
阶段2: Compact 压缩阶段,标记完所有垃圾之后,就释放它们的内存空间,内存空间变得不连续,在堆中移动这些对象,使他们重新从堆基地址开始连续排列,类似于磁盘空间的碎片整理。
垃圾收集算法的改善 - 分代
当进行垃圾收集时,扫描所有对象将是非常费时间的,为了优化这个过程,出现了分代算法。分代算法的精髓就是“对象存在的时间越长则可能越重要,越有可能应该保留”。
.NET将堆分成3个代龄区域: Gen 0、Gen 1、Gen 2;
所有的对象在创建时都放在第0代。如果Gen 0 heap内存达到阈值,则触发0代GC,(此时仅仅会扫描0代的对象)0代GC后Gen 0中幸存的对象进入Gen1。如果Gen 1的内存达到阀值,则进行1代GC,1代GC将Gen 0 heap和Gen 1 heap一起进行回收,幸存的对象进入Gen2。2代GC将Gen 0 heap、Gen 1 heap和Gen 2 heap一起回收,Gen 0和Gen 1比较小,这两个代龄加起来总是保持在16M左右;Gen2的大小由应用程序确定,可能达到几G,因此0代和1代GC的成本非常低,2代GC称为full GC,通常成本很高。粗略的计算0代和1代GC应当能在几毫秒到几十毫秒之间完成,Gen 2 heap比较大时,full GC可能需要花费几秒时间。大致上来讲.NET应用运行期间,2代、1代和0代GC的频率应当大致为1:10:100。
有了分代算法,程序就不需要扫描所有对象了。
(http://www.cnblogs.com/springyangwc/archive/2011/06/13/2080149.html)
其他
垃圾回收通常不需要人工干预,但也有少数时候是例外。因为垃圾回收只适用于托管对象(更准确的说是只适用于可终结的对象),对于非托管对象,则是CLR不能控制或者管理的部分,这些资源有很多,比如文件流,数据库的连接,系统的窗口句柄,打印机资源等等……这些资源一般情况下不存在于Heap(内存中用于存储对象实例的地方)中。不享受垃圾回收,我们仍然要自己去释放内存。此时我们有很多选择。例如我们将要预见有数个较大的资源将要创建,而系统可能已经没有那么多资源在托管堆上,那么我们可以做的事情有:
1. 强制调用垃圾回收,清除托管堆上所有的没用的托管对象
2. 使用dispose()或对任何对象实现IDisposable接口,使对象变为可处置的,然后显式或隐式调用dispose(),精准的干掉这个对象
3. 对非托管对象实现finalize()方法(只能通过定义一个解构函数才能实现),使对象变为可终结的,然后该对象就可以享受垃圾回收器的自动清除好处
其中2和3可以合并,而这也是标准做法。
强制垃圾回收
强制垃圾回收需要调用GC.Collect方法,其可以强制进行一次垃圾回收。这个方法有重载版本,可以指定回收的代。在调用完该方法之后,可以马上调用WaitForPendingFinalizers(),可以确定在程序继续执行之前,所有可终结的对象都必须执行所有必要的清除工作。
重写Finalize方法
其实我们没办法override Finalize方法,只能通过定义一个解构函数隐式的做到这件事,这是因为,当编译器执行解构函数时,将自行在被隐式重写的finalize方法中增加许多代码。我们可以在解构函数中加入代码清除非托管资源,甚至还可以打印东西到控制台上,不过,解构函数的执行时间是无法预测的(在垃圾回收时)。对非托管对象定义一个解构函数之后,其就变成可终结的了,然后我们就可以期待系统以处置托管对象相同的方法处置它。不过,这个方法较为被动(仍然要等待垃圾回收),如果要主动的清除,则要实现IDisposable接口,使对象变为可处置的,然后显式或隐式调用dispose()。
解构函数(终结器)和dispose()
(http://www.cnblogs.com/luminji/archive/2011/03/29/1997812.html)
如果我们的类型使用到了非托管资源,或者需要显式释放的托管资源,那么,最好让类型继承接口IDisposable。这相当于是告诉调用者,该类型是需要显式释放资源的,你需要调用我的Dispose方法。调用dispose和垃圾收集毫无关系,垃圾收集也不会自动调用dispose()。所以,如果用户记得调用,则不需要再调用解构函数,而如果用户忘了调用,需要解构函数善后(正式的方法就是这样做的)。一般来说,用using块包住可以令系统自行调用dispose(当离开using块之后)。
正式的处置方法
微软定义了一个正式的对释放资源的处置模式,其注意到以下几个问题:
1. 可以多次调用dispose()而不出问题(如果已经调用过,再次调用不应该触发清理过程)
2. 如果调用了dispose(),则不需要再次调用终结器
3. 对象既是可终结的也是可主动释放内存的
class BaseClass : IDisposable { bool disposed = false; //供外部调用 public void Dispose() { //进行垃圾回收 Dispose(true); //通知垃圾回收不再调用终结器,因为我们已手动释放了内存 GC.SuppressFinalize(this); } //内部 protected virtual void Dispose(bool disposing) { if (disposed) return; if (disposing) { //清理托管资源 } //清理非托管资源 //让类型知道自己已经被释放 disposed = true; } /// <summary> /// 必须,以备程序员忘记了显式调用Dispose方法 /// </summary> ~BaseClass() { //必须为false Dispose(false); } }
其中解构函数是一种特殊的函数,其和类的名称相同,前加一波浪号~。提供解构函数的全部意义在于不能奢望类型的调用者肯定会主动调用Dispose方法,基于解构函数会被垃圾回收器调用这个特点,终结器被用做资源释放的补救措施。