Microsoft.Net框架程序设计学习笔记(34):终止化操作

终止化操作

  任何封装了非托管资源的类型,如文件、网络连接、套接字等,都必须支持一种称作终止化的操作。终止化操作允许一种资源在它所占用的内存被回收之前执行一些清理工作。要提供终止化操作,我们必须为类型重写一个名为Finalize的方法,该方法在Object中被定义为受保护的虚方法。当垃圾收集器判定一个对象为可收集的垃圾时,它便会调用该对象的Finalize方法。Finalize方法的实现通常便是调用CloseHandle函数。

  然而,在C#中实现Finalize方法并非直接重写Finalize方法,C#编译器为我们提供了与C++析构函数类似特殊语法来定义Finalize方法。下例演示了如何定义一个封装着非托管资源的类型:

  
    
using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.InteropServices;

namespace TestFinalize
{
public sealed class OSHandle
{
// 释放非托管资源
[DllImport( " Kernel32 " )]
public extern static Boolean CloseHandle(IntPtr handle);

// 一个非托管资源的Win32句柄
private IntPtr handle;

// 构造器初始化handle句柄
public OSHandle(IntPtr handle)
{
this .handle = handle;
}

// 当垃圾收集执行时,下面的析构器(Finalize)方法将被调用
// 经编译后,该方法实际被更名为Finalize方法
~ OSHandle()
{
// 关闭非托管资源句柄
CloseHandle(handle);
}

// 返回所封装的handle句柄
public IntPtr ToHandle() { return handle; }

// 稳式转型操作符用于返回所封装的handle句柄
public static implicit operator IntPtr(OSHandle osHandle)
{
return osHandle.ToHandle();
}
}
}

  当某个时刻垃圾收集器判定OSHandle对象实例为可收集垃圾时,它会看到该类型定义有一个Finalize方法,于是便会调用该方法。在Finalize方法返回后的某个时刻,该OSHandle对象在托管堆中的内存才会被回收。

  注意:虽然C#中的Finalize方法原型类似于C++的析构函数,但调用该方法并不像C++中那样类型实例对象就被确定性地析构。CLR不支持对象的确定性,调用Finalize方法的结果仅仅是该对象所占内存在之后的某个时刻可被垃圾回收而已。

  设计一个类型时,我们应该尽可能地避免使用Finalize方法,原因如下:

  1. 终止化对象(即重写了Finalize方法的类型对象)的代龄会被提升,必须至少经历两次垃圾回收(第1次将对象加入终止化可达队列后再执行Finalize方法,第2次才能真正释放内存)才能释放所占内存。另外,所有被终止化对象直接引用或间接引用的对象代龄也将被提升。
  2. 终止化对象的分配花费的时间较长,因为指向它们的指针必须放在终止化链表上。
  3. 强制垃圾收集器执行Finalize方法会极大操作应用程序的性能。
  4. 我们不能控制Finalize方法何时执行,对象可能会一直占着非托管资源,直到出现垃圾收集。
  5. CLR不对Finalize的执行顺序作任何保证。

  如果有任何异常在Finalize方法中未经捕获而逃脱,那么CLR会忽略它,并继续调用其他对象的Finalize方法。

  实现一个Finalize方法最常见的原因便是释放对象所占有的非托管资源。实际上,在终止化操作中,我们应该避免编写代码访问其他托管对象或托管静态方法。原因是这些对象的类型也可能实现了Finalize方法,而它们有可能首先调用,从而将这些对象置于一个不可预期的状态。

  如果我们在构造终止化对象时实例构造器抛出了异常,导致相关的非托管资源亦未能正确创建,这时我们应该调用GC.SuppressFinalize方法阻止Finalize方法的执行。见下例:

  
    
using System;
using System.Collections.Generic;
using System.Text;
using System.IO;

namespace TestFinalize
{
class TempFile
{
string filename;
public FileStream fs;

public TempFile( string filename)
{
try
{
// 下一行代码创建文件,可能抛出异常
fs = new FileStream(filename, FileMode.Create);
this .filename = filename;
}
catch
{
// 告诉垃圾收集器不要调用Finalize方法
GC.SuppressFinalize( this );
// 向上传递异常
throw ;
}
}

~ TempFile()
{
File.Delete(filename);
}
}
}

调用Finalize方法的条件

  有4种事件会导致一个对象的Finalize方法被调用:

  1. 第0代对象充满:最常见的导致调用Finalize方法的一种方式,通常在分配新对象的时候发生。
  2. 显式调用System.GC的静态方法Collect执行垃圾收集。
  3. CLR卸载应用程序域,调用AppDomain.CurrentDomain.IsFinalizingForUnload()方法可知当前CLR是否正在卸载应用程序域。
  4. CLR被关闭:当一个进程正常中断时,它会试图关闭CLR,调用Environment.HasShutdownStarted可知当前是否正在关闭CLR。

  CLR使用一个特殊的专用线程来调用Finalize方法。对于第1、2、3种事件来说,如果有Finalize方法进入了一个无限循环,那么这个特殊线程将被阻塞,其他的Finalize方法得不到调用。对于第4种事件来说,每个Finalize方法会有大约2秒种的运行时间,超出该时间,CLR将中断该进程。另外,如果调用所有对象的Finalize方法超过了40秒,那么CLR也会中断该进程。

终止化操作的内部机理

  当应用程序创建一个新对象时,如果该对象的类型定义了Finalize方法,那么在该类型的实例构造器被调用前,指向该对象的一个指针将被放到一个称为终止化链表的数据结构里。该链表的每一个条目都引用着一个终止化对象,这是在告诉垃圾收集器在回收这些对象的内存之前要首先调用它们的Finalize方法。

  当垃圾收集开始时,垃圾收集器首先哪些对象可被收集。然后,垃圾收集器扫描终止化链表以查找链表中是否包含可收集对象的指针。如果找到这样的指针,它会被从终止化链表上移除,并添加到一个称作终止化可达队列的数据结构上。在终止化可达队列中出现的对象表示该对象的Finalize方法即将被调用。经过此次垃圾收集后,这些可回收的终止化对象仅仅是被添加到了终止化可达队列上,由于Finalize方法没有执行,它们占有的内存还不能被回收。

  CLR中有一个特殊的高优先级的线程专门用于调用Finalize方法。当终止化可达队列为空时,该线程睡眠。但当终止化可达队列中有条目出现时,该线程被唤醒,开始把每个条目从终止化可达队列中移除,并调用每个对象的Finalize方法。

  其实可以把终止化可达队列看作和全局变量、静态变量一样的根。这些根指向的终止化对象是可达对象,所以暂时还不能被收集,必须等终止化对象从队列中移除(即队列条目不再指向待收集的终止化对象),执行Finalize方法后才能被收集。

  再等下一次垃圾收集执行时,它会看到终止化对象已成为真正的垃圾对象,因为应用程序的根不再指向它,终止化可达队列也不再指向它,这时对象的内存才会被回收。这样,终止化对象必须执行至少两次垃圾收集才能释放所占内存。

你可能感兴趣的:(Microsoft)