有些对象需要显示地销毁代码来释放资源,比如打开的文件资源,锁,操作系统句柄和非托管对象。在.NET中,这就是所谓的对象销毁,它通过IDisposal接口来实现。不再使用的对象所占用的内存管理,必须在某个时候回收;这个被称为无用单元收集的功能由CLR执行。
对象销毁和垃圾回收的区别在于:对象销毁通常是明确的策动;而垃圾回收完全是自动地。换句话说,程序员负责释放文件句柄,锁,以及操作系统资源;而CLR负责释放内存。
本章将讨论对象销毁和垃圾回收,还描述了C#处理销毁的一个备选方案--Finalizer及其模式。最后,我们讨论垃圾回收器和其他内存管理选项的复杂性。
对象销毁 | 垃圾回收 |
1)IDisposal接口2) Finalizer | 垃圾回收 |
对象销毁用于释放非托管资源 | 垃圾回收用于自动释放不再被引用的对象所占用的内存;并且垃圾回收什么时候执行时不可预计的 |
为了弥补垃圾回收执行时间的不确定性,可以在对象销毁时释放托管对象占用的内存 |
.NET Framework定义了一个特定的接口,类型可以使用该接口实现对象的销毁。该接口的定义如下:
public interface IDisposable{void Dispose();}
C#提供了鴘语法,可以便捷的调用实现了IDisposable的对象的Dispose方法。比如:
using (FileStream fs = new FileStream ("myFile.txt", FileMode.Open)){// ... Write to the file ...}
编译后的代码与下面的代码是一样的:
FileStream fs = new FileStream ("myFile.txt", FileMode.Open);try{// ... Write to the file ...}finally{if (fs != null) ((IDisposable)fs).Dispose();}
finally语句确保了Dispose方法的调用,及时发生了异常,或者代码在try语句中提前返回。
在简单的场景中,创建自定义的可销毁的类型值需要实现IDisposable接口即可
sealed class Demo : IDisposable{public void Dispose(){// Perform cleanup / tear-down....}}
请注意,对于sealed类,上述模式非常适合。在本章后面,我们会介绍另外一种销毁对象的模式。对于非sealed类,我们强烈建议时候后面的那种销毁对象模式,否则在非sealed类的子类中,也希望实现销毁时,会发生非常诡异的问题。
Framework在销毁对象的逻辑方面遵循一套规则,这些规则并不限用于.NET Framework或C#语言;这些规则的目的是定义一套便于使用的协议。这些协议如下:
这些规则同样也适用于我们平常创建自定义类型,尽管它并不是强制性的。没有谁能阻止你编写一个不可销毁的方法;然而,这么做,你的同事也许会用高射炮攻击你。
对于第三条规则,一个容器对象自动销毁其子对象。最好的一个例子就是,windows容器对象比如Form对着Panel。一个容器对象可能包含多个子控件,那你也不需要显示地销毁每个字对象:关闭或销毁父容器会自动关闭其子对象。另外一个例子就是如果你在DeflateStream包装了FileStream,那么销毁DeflateStream时,FileStream也会被销毁--除非你在构造器中指定了其他的指令。
Close和Stop
有一些类型除了Dispose方法之外,还定义了Close方法。Framework对于Close方法并没有保持完全一致性,但在几乎所有情况下,它可以:
对于后者一个典型的例子就是IDbConnecton类型,一个Closed的连接可以再次被打开;而一个Disposed的连接对象则不能。另外一个例子就是Windows程序使用ShowDialog的激活某个窗口对象:Close方法隐藏该窗口;而Dispose释放窗口所使用的资源。
有一些类定义Stop方法(比如Timer或HttpListener)。与Dipose方法一样,Stop方法可能会释放非托管资源;但是与Dispose方法不同的是,它允许重新启动。
销毁对象应该遵循的规则是“如有疑问,就销毁”。一个可以被销毁的对象--如果它可以说话--那么将会说这些内容:
“如果你结束对我的使用,那么请让我知道。如果只是简单地抛弃我,我可能会影响其他实例对象、应用程序域、计算机、网络、或者数据库”
如果对象包装了非托管资源句柄,那么经常会要求销毁,以释放句柄。例子包括Windows Form控件、文件流或网络流、网络sockets,GDI+画笔、GDI+刷子,和bitmaps。与之相反,如果一个类型是可销毁的,那么它会经常(但不总是)直接或间接地引用非托管句柄。这是由于非托管句柄对操作系统资源,网络连接,以及数据库锁之外的世界提供了一个网关(出入口),这就意味着使用这些对象时,如果不正确的销毁,那么会对外面的世界代码麻烦。
但是,遇到下面三种情形时,不要销毁对象
第一种情况很少见。多数情形都可以在System.Drawing命名空间下找到:通过静态成员或属性获取的GDI+对象(比如Brushed.Blue)就不能销毁,这是因为该实现在程序的整个生命周期中都会用到。而通过构造器得到的对象实例,比如new SolidBrush,就应该销毁,这同样适用于通过静态方法获取的实例对象(比如Font.FromHdc)。
第二种情况就比较常见。下表以System.IO和System.Data命名空间下类型举例说明
类型 | 销毁功能 | 何时销毁 |
MemoryStream | 防止对I/O继续操作 | 当你需要再次读读或写流 |
StreamReader,StreamWriter | 清空reader/writer,并关闭底层的流 | 当你希望底层流保持打开时(一旦完成,你必须改为调用StreamWriter的Flush方法) |
IDbConnection | 释放数据库连接,并清空连接字符串 | 如果你需要重新打开数据库连接,你需要调用Close方法而不是Dispose方法 |
DataContext(LINQ to SQL) | 防止继续使用 | 当你需要延迟评估连接到Context的查询 |
第三者情况包含了System.ComponentModel命名空间下的这几个类:WebClient, StringReader, StringWriter和BackgroundWorker。这些类型有一个共同点,它们之所以是可销毁的是源于它们的基类,而不是真正的需要进行必要的清理。如果你需要在一个方法中使用这样的类型,那么在using语句中实例化它们就可以了。但是,如果实例对象需要持续一段较长的时间,并记录何时不再使用它们以销毁它们,就会给程序带来不惜要的复杂度。在这样的情况下,那么你就应该忽略销毁对象。
正因为IDisposable实现类可以使用using语句来实例化,因而这可能很容易导致该实现类的Dispose方法延伸至不必要的行为。比如:
public sealed class HouseManager : IDisposable{public void Dispose(){CheckTheMail();}...}
想法是该类的使用者可以选择避免不必要的清理--简单地说就是不调用Dispose方法。但是,这就需要调用者知道HouseManager类Dispose方法的实现细节。及时是后续添加了必要的清理行为也破坏了规则。
public void Dispose(){CheckTheMail(); // NonessentialLockTheHouse(); // Essential}
在这种情况下,就应该使用选择性销毁模式
public sealed class HouseManager : IDisposable{public readonly bool CheckMailOnDispose;public Demo (bool checkMailOnDispose){CheckMailOnDispose = checkMailOnDispose;}public void Dispose(){if (CheckMailOnDispose) CheckTheMail();LockTheHouse();}...}
这样,任何情况下,调用者都可以调用Dispose--上述实现不仅简单,而且避免了特定的文档或通过反射查看Dispose的细节。这种模式在.net中也有实现。System.IO.ComPRession空间下的DeflateStream类中,它的构造器如下
public DeflateStream (Stream stream, CompressionMode mode, bool leaveOpen)
非必要的行为就是在销毁对象时关闭内在的流(第一个参数)。有时候,你希望内部流保持打开的同时并销毁DeflateStream以执行必要的销毁行为(清空bufferred数据)
这种模式看起来简单,然后直到Framework 4.5,它才从StreamReader和StreamWriter中脱离出来。结果却是丑陋的:StreamWriter必须暴露另外一个方法(Flush)以执行必要的清理,而不是调用Dispose方法(Framework 4.5在这两个类上公开一个构造器,以允许你保持流处于打开状态)。System.Security.Cryptography命名空间下的CryptoStream类,也遭遇了同样的问题,当需要保持内部流处于打开时你要调用FlushFinalBlock销毁对象。
在一般情况下,你不要在对象的Dispose方法中清除该对象的字段。然而,销毁对象时,应该取消该对象在生命周期内所有订阅的事件。退订这些事件避免了接收到非期望的通知--同时也避免了垃圾回收器继续对该对象保持监视。
设置一个字段用以指明对象是否销毁,以便在使用者在该对象销毁后访问该对象抛出一个ObjectDisposedException,这是非常值得做的。一个好的模式就是使用一个public的制度的属性:
public bool IsDisposed { get; private set; }
尽管技术上没有必要,但是在Dispose方法清除一个对象所拥有的事件句柄(把句柄设置为null)也是非常好的一种实践。这消除了在销毁对象期间这些事件被触发的可能性。
偶尔,一个对象拥有高度秘密,比如加密密钥。在这种情况下,那么在销毁对象时清除这样的字段就非常有意义(避免被非授权组件或恶意软件发现)。System.Security.Cryptography命令空间下的SymmetricAlgorithm类就属于这种情况,因此在销毁该对象时,调用Array.Clear方法以清除加密密钥。
无论一个对象是否需要Dispose方法以实现销毁对象的逻辑,在某个时刻,该对象在堆上所占用的内存空间必须释放。这一切都是由CLR通过GC自动处理. 你不需要自己释放托管内存。我们首先来看下面的代码
public void Test(){byte[] myArray = new byte[1000];}
当Test方法执行时,在内存的堆上分配1000字节的一个数组;该数组被变量myArray引用,这个变量存储在变量栈上。当方法退出后,局部变量myArray就失去了存在的范畴,这也意味着没有引用指向内存堆上的数组。那么该孤立的数组,就非常适合通过垃圾回收机制进行回收。
垃圾回收机制并不会在一个对象变成孤立的对象之后就立即执行。与大街上的垃圾收集不一样,.net垃圾回收是定期执行,尽享不是按照一个估计的计划。CLR决定何时进行垃圾回收,它取决于许多因素,比如,剩余内存,已经分配的内存,上一次垃圾回收的时间。这就意味着,在一个对象被孤立后到期占用的内存被释放之间,有一个不确定的时间延迟。该延迟的范围可以从几纳秒到数天。
垃圾回收和内存占用垃圾收集试图在执行垃圾回收的时间与程序的内存占用之间建立一个平衡。因此,程序可以占用比它们实际需要更多的内存,尤其特现在程序创建的大的临时数组。你可以通过Windows任务管理器监视某一个进程内存的占用,或者通过编程的方式查询性能计数器来监视内存占用:// These types are in System.Diagnostics:string procName = Process.GetCurrentProcess().ProcessName;using (PerformanceCounter pc = new PerformanceCounter("Process", "Private Bytes", procName))Console.WriteLine (pc.NextValue());上面的代码查询内部工作组,返回你当前程序的内存占用。尤其是,该结果包含了CLR内部释放,以及把这些资源让给操作系统以供其他的进程使用。 |
根就是指保持对象依然处于活着的事物。如果一个对象不再直接或间接地被一个根引用,那么该对象就适合于垃圾回收。
一个跟可以是:
正在执行的代码可能涉及到一个已经删除的对象,因此,如果一个实例方法正在执行,那么该实例方法的对象必然按照上述方式被引用。
请注意,一组相互引用的对象的循环被视作无根的引用。换一种方式,也就是说,对象不能通过下面的箭头指向(引用)而从根获取,这也就是引用无效,因此这些对象也将被垃圾回收器处理。
新闻热点
疑难解答