.NET程序运行在托管环境中,开发者必须从.NET CLR的绝对来思考,才可以充分发挥这套环境的优势。必须理解垃圾回收机制和对象生存期。
.NET提供GC来控制托管内存,非托管的资源需要开发者来控制。例如数据库连接、GDI+对象、COM对象以及其他一些系统对象。
垃圾回收器每次运行是都会压缩托管堆,以便把其中的活动对象安排在一起,使得空闲内存能够形成一块连续的区域。
有两种机制可以控制非托管资源的生存期,一种是finalizer(终止器),另一种是IDisposable接口。finalizer是一种防护机制,可以确保对象总是能够把非托管资源释放掉,但这种机制有很多缺陷,所以应该考虑通过IDisposable接口来及时将资源返回给系统。
finalizer,当垃圾回收器把对象判定为垃圾之后,他会择机调用该对象的finalizer,但开发者并不知道具体的时机,你只能确认在大多数情况下,当对象变得不可达之后,其finalizer就会得到调用,但执行的并不及时。依赖finalizer会降低程序的性能,因为垃圾回收器需要执行更多的工作才能终止这些对象。如果GC发现某个对象已成为垃圾,但该对象还要执行finalizer,要等finalizer执行完后才能将其移走。调用finallizer的那个线程并不是GC所在线程。GC在每一个周期里会把包好finalizer但是尚未执行的那些对象放在队列中,以便安排其finalizer的运行工作,而不含finalizer的对象则会直接从内存清掉。等到下一周期,GC会把已经执行了finalizer的对象删掉,因此局部finalizer的对象还需在内存中多待一段时间才能被GC清理掉。
为了优化垃圾回收工作,.NET的垃圾回收器定义了世代(generation)这个概念,以便尽快确定那些最有可能变成垃圾的对象。上一次收集完垃圾之后才创建出来的对象叫做第0代对象,如果其中某些对象在这次清扫之后还留在内存,那就变为第2代对象,若经过两次或更多次的清理之后它还留在内存中,则变为第二代对象。把对象分成不同的世代,可以将生存期较短的对象与全程伴随的对象区隔开。
GC每次循环,都会判断第0代对象是否垃圾,但每执行10次循环,才会把第1代对象连同第0代对象检测一遍,而第2代对象则是每100次循环才检测一遍。重新思考一下finalizer就会发现,与不带finalizer的对象相比,这种对象要在内存中多待9个周期,如果还没有得以终结则进入第2代,再等100个周期,才能由GC来收集它。
尽管花了很多时间来解释finalizer的缺点,但有些场合还是要释放资源,这可以用IDisposable接口及其标准的dispose模式来解决。
下一节将讲一些具体的技巧,帮你再托管环境下创建更为高效的程序。