对象的生命周期
要了解对象的生命周期就先要了解对象创建的过程和.net垃圾回收机制。
定义了一个类后,就可以使用new来创建对象。new返回的不是真正对象本身,而是指向堆上对象的引用。这个引用变量保存在栈内,以供应用程序以后使用。
当new创建对象后,垃圾回收器就会在对象不再需要时将其销毁。这时有人可能会问:垃圾回收器怎么知道对象什么时候不再需要呢?垃圾回收器会检查对象在代码库的任何部分都不可访问时,垃圾回收器具就会把它从堆中销毁。举个简单的例子:
public static void MakeCar()
{
//如果myCar只引用Car对象,当方法返回的时候它就会被销毁
Car myCar = new Car();
}
这个方法调用结束后,myCar引用就再也不可访问了,相关联的Car对象现在就是垃圾回收的候选目标。但不能保证这个对象会在方法结束后就立即被垃圾回收器销毁。.net有了这个垃圾回收器后与C++对比,很明显大大简化了应用程序的开发。我们不用手机删除分配在堆上的对象,不会因此而造成内在泄漏的问题。通过允许垃圾收集器负责销毁对象,内容的麻烦都交给CLR来处理了。
因为我们经常需要在应用程序中频繁地分配对象,托管堆的空间最终会被用完。而当C#编译器遇到new关键字时,它会在方法的实现中加入一条CIL newobj指令。当处理newobj指令时,如果CLR判定托管堆没有足够的空间来分配所请求的类型,它会执行一次垃圾回收来尝试释放内存。垃圾回收的第一个法则是:
法则1:如果托管堆没有足够的内存来分配所请求的对象,不会进行垃圾回收。
对象的代
当CLR寻找不可访问对象时,它肯定不会逐个检查托管堆上的每一个对象,因为这样的话会花费大量的时间,效率非常的低。为了提高查找的效率,堆上的每一个对象被指定为属于某“代”。代的设计思想很简单:对象在堆上存在的时间越长,它就更可能应该保留。对象的代分为三种:
第0代:从没有被标记为回收的新分配的对象。
第1代:在上一次垃圾回收中只被标记,但没有被回收的对象。
第2代:在一次以上的垃圾回收后仍然没有被回收的对象。
垃圾回收器首先要检查所有的第0代对象。如果标记和清除这些对象得到了所需数量的空闲内存,任何没有被回收的对象都被提升到第1代。如果算上所有的第0代对象后仍然需要更多的内存,就会检查第1代对象并相应地进行回收。没有被回收的第1代对象随后被提升为第2代。如果垃圾回收器仍然需要更多的内存,它就会检查第2代的对象。如果第2代对象在垃圾回收后仍然存在,它仍然是第2代对象,因为这是预定义的对象代的上限。
强制垃圾回收
基类库提供了System.GC的类,它可以使用一个静态成员集合与垃圾回收器进行交互。.net垃圾回收器的功能是代替我们管理内存。但我们可以通过GC.Collect强制地进行一次垃圾回收,如下例子:
static void Main(string[] args)
{
//强制一次垃圾回收,并等待每一个对象都被终结
GC.Collect();
GC.WaitForPendingFinalizers();
}
当手动强制垃圾回收时,应该总是调用GC.WaitForPendingFinalizers()。这样你可以稍等片刻,经确定在程序继续执行之前,所有可终结的对象都必须执行任何必要的清除工作。在底层,GC.WaitForPendingFinalizers()会在回收过程中挂起调用的“线程”。也可以给GC.Collect()方法提供一个数值,例如想让CLR只检查第0代对象,可以GC.Collect(0)。
可终结对象和可处置对象
.net的基类System.Object定义了Finalize()的虚方法。在从内存删除这个对象之前,垃圾回收器会调用对象的Finalize()方法。
法则2:重写Finalize()的唯一原因是,C#类通过PInvoke或复杂的COM互操作性任务使用了非托管资源(典型情况是通过System.Runtime.InteropServices.Marshal类型)。
如果创建了一个使用非托管资源的类,且重写Finalize(),在C#中不能用overrice来做。但可以使用析构函数来达到同样的效果。下面是自定义的终结器:
class MyResourceWrapper
{
~class MyResourceWrapper()
{
//清除这里非托管的资源
}
}
Finalize()方法的作用是保证.net对象能在垃圾回收时清除非托管资源,如果创建了一个不使用非托管褓的类型,终结是没有用的。
处理对象清理工作除了重写Finalize()之外,还可以实现IDisposble对象:
public interface IDisposable
{
void Dispose()
}
实现这个接口的对象可称为可处置对象。值得注意的是,Dispose()方法不只负责释放一个对象的非托管资源,还应该对任何它包含的可处置对象调用Dispose()。与Finalize()不一样,在Dispose()方法中与其他托管对象通信是安全的。因为垃圾回收器并不支持IDisposable接口,不会调用Dispose()。
法则3:如果对象支持IDisposable,总是要对任何直接创建的对象调用Dispose()。应该认为,如果类设计者选择支持Dispose()方法,这个类型就需要执行清除工作。
当然我们也可以构建一个既可终结,也可处置的对象,如下例子:
public class MyResourceWrapper : IDisposable
{
//如果对象用户忘记调用Dispose(),垃圾回收器会调用这个方法
~MyResourceWrapper ()
{
//清除任何内部的非托管资源
//不要调用任何托管对象的Dispose()
}
public void Dispose()
{
//在这里清除非托管资源
//在其他包含的可处置对象上调用Dispose()
//如果用户调用了Dispose()就不需要终结,因为跳过终结
GC.SuppressFinalize(this);
}
}
Dispose()方法已被修改为调用GC.SuppressFinalize(this),它将会通知CLR在对象被垃圾回收时不再需要调用析构函数,因为非托管的资源已经通过Dispose()逻辑被释放了。
这时候我们可以知道垃圾回收器公当它不能从托管堆获得需要的内在时才运行。在有了垃圾回收时,我们大可以放心了,因为.net的回收算法已经通过使用对象代、用于对象终结的辅助纯种和专门承载大对象的托管堆优化过。
参考文献:《C#与.NET3.0高级程序设计》