您的位置:首页 > 其它

垃圾回收、可终结Finalizable、可处置Disposable

2012-11-27 15:20 183 查看
在.NET中,CLR通过垃圾回收(garbage collection)来管理已分配的对象,C#程序员从来不直接从内存删除一个托管对象(C#语言中没有delete关键字)。.NET对象被分配到一块叫做托管堆(managed heap)的内存区域上,在那里他们会在“将来某一时刻”被GC自动销毁。在本文中,将说明C#中的对象生命周期、垃圾回收和创建可终结可处置的安全类型。

对象生命周期

类只是一个蓝图,描述这个类型的实例在内存中看起来是什么样子。定义了类后,就可以使用new关键字创建任意数量的对象,但是new关键字返回的是一个指向堆上对象的引用,而不是真正的对象本身。这个引用变量保存在栈内,以供应用程序使用。默认情况下,一个引用类型的赋值将产生一个对该堆上同一个对象的新引用。

下图说明了类、对象和引用的关系:



当C#编译器遇到new关键字时,它会在方法的实现中加入一条CIL newObj指令。CIL newObj指令的核心任务有:

a.计算分配对象所需要的总内存数(包含类型的成员变量和类型的基类所需的必要内存);

b.检查托管堆,确保有足够的空间来放置要分配的对象。如果空间足够,调用类型的构造函数,最终将内存中新对象的引用返回给调用者;

c.在将引用返回给调用者之前,移动下一个对象的指针(将分配的新对象的指针),指向托管堆上的下一个可用的位置。

该基本过程如下图所示:



当处理newObj指令时,如果CLR判定托管堆没有足够的空间来分配所请求的类型,它会执行一次垃圾回收来尝试释放内存。

对象创建后,对于值类型的对象,当它们越出定义的作用域时(如:方法内的临时变量,在方法返回后),这个类型的对象就消亡了;对于引用类型的对象,它们分配在托管堆上,这些对象一直保留在内存中,直到.NET垃圾回收器将它们销毁。

垃圾回收

如前所述,引用类型的对象由new创建后,垃圾回收器将在不再需要时才将其销毁。问题是垃圾回收器如何判断一个对象什么时候不再需要呢?简单的说是,当一个对象从代码库的任何部分都不可达(即对象没有根)时,垃圾回收器就将其标记为垃圾,成为垃圾回收的候选目标。

为了优化CLR寻找不可达对象的过程,堆上的每一个对象被指定为属于某“代(generation)”,代的思路很简单:对象在堆上存在的时间越长,它就更应该保留(如实现Main方法的对象,应用程序对象),相反最近才放在堆上的对象可能很快就不可达了(如在方法作用域中创建的对象,本地变量)。基于此,每一个堆上的对象都属于下列某代:

第0代:从没有被标记为回收的新分配的对象

第1代:在上一次垃圾回收中没有被回收的对象(即它被标记为回收,但因为已经获取了足够的堆空间而没有被回收)

第2代:在一次以上的垃圾回收后仍然没有被回收的对象

垃圾回收时,垃圾回收器首先处理第0代对象,如果不够再处理第1代、甚至第2代对象,剩余的第0代对象提升为第1代,以此类推,但上限为第2代。

当确实发生回收时,垃圾回收器暂时挂起所有再当前进程中所有活动的线程,以保证应用程序在回收过程中不会访问堆,一旦垃圾回收周期完成,就允许挂起的线程继续它们的工作。

构建可终结类型

在System.Object类中,定义了Finalize()虚方法,该方法的作用是保证.NET对象能在垃圾回收时清除非托管资源。非托管资源(如原始的文件句柄、数据库连接等)是通过使用PInvoke(平台调用)服务直接调用操作系统的API,或通过一些复杂的COM交互获得的。显然该方法的默认实现什么也不做。

由于有垃圾回收,大多数C#类都不需要重写Finalize()方法来显式的指定清理逻辑,一切托管对象最终都会被垃圾回收。只是在使用非托管资源时,才可能需要自定义清理逻辑。Finalize()方法是受保护的,所以不可能直接调用一个对象的Finalize()方法,在从内存回收这个对象之前,垃圾回收器会自动调用对象的Finalize()方法(如果支持的话),细节见后面的描述。

注:在结构类型上重写Finalize()是不合法的,因为结构是值类型,它们本来就从不分配在堆上

在C#中重写Finalize()方法比较奇怪,不能用预期的override关键字来做:

public class MyResourceWrapper
{
//编译器错误!
protected
override void Finalize() {}
}

当想重写Finalize()方法时,可以使用如下的析构函数语法。

class MyResourceWrapper
{
~MyResourceWrapper()
{
//这里清除非托管资源,这里仅是测试
Console.Beep()
}
}

之所以用这种替代形式,是因为C#编译器处理一个析造函数时,它将自动在Finalize方法中增加必需的错误检测代码,保证基类的Finalize()方法总是被执行。如果用ildasm.exe查看这个C#析构函数,将看到:

.method family hidebysig virtual instance
void
Finalize() cil managed
{
// Code size 13 (0xd)

.maxstack 1
.try
{
IL_0000: ldc.i4 0x4e20
IL_0005: ldc.i4 0x3e8
IL_000a: call
void [mscorlib]System.Console::Beep(int32, int32)
IL_000f: nop
IL_0010: nop
IL_0011: leave.s IL_001b
} // end .try
finally
{
IL_0013: ldarg.0
IL_0014:
call instance void [mscorlib]System.Object::Finalize()
IL_0019: nop
IL_001a: endfinally
} // end handler
IL_001b: nop
IL_001c: ret
} // end of method MyResourceWrapper::Finalize

对于没有使用非托管资源的类型,终结是没有用的。事实上,只要有可能的话,就应该在设计类型时避免提供Finalize()方法,因为终结是要花费时间的。

当在托管堆上分配对象时,运行库自动确定该对象是否提供一个自定义的Finalize()方法,如果是则对象被标记为可终结的,同时一个指向该对象的指针被保存在垃圾回收器维护的内部队列(终结队列)中。当垃圾回收器确定释放一个对象时,它检查终结队列上的每一个项,并将对象从堆上复制到另外一个托管结构终结可达表上。下一个垃圾回收时将产生另一个线程,为每一个在可达表中的对象调用Finalize()方法。因此,为了真正终结一个对象,至少要进行两次垃圾回收。总而言之,尽管对象的终结能够保证对象可以清除非托管资源,但它本质上仍然时非确定的,二期由于额外的幕后处理,速度会变得相当慢。

构建可处置类型

很多非托管资源都非常宝贵,所以它们应该尽可能快地被清除,固然可以通过Finalize()方法在垃圾回收时清除这些资源,但是这样会有将对象放在终结队列上的性能损失,而且必须等待垃圾回收器触发类的终结逻辑。因此需要另外一种处理对象清理工作的技术--实现IDisposable接口,该接口定义了一个名为Dispose()的方法,可以在该方法内自定义必要的对象清理逻辑。

实现IDisposable接口,就是假设当对象用户不再使用这个对象时,会在这个对象引用离开作用域之前手工地调用Dispose(),这样对象可以执行非托管资源的清理。与Finalize()方法不同,在Dispose方法中与其他托管对象通信时很安全的,因为垃圾回收器并不支持IDisposable接口,永远不会调用Dispose(),因此当对象的用户调用这个方法时,对象仍然在托管堆上,并可以访问所有其他分配在堆上的对象。

注:与重写Finalize()不同,结构和类类型都可以支持IDisposable

处理实现了IDisposable的托管对象时,为保证类型的Dispose()方法在出现运行时异常时也会调用,通常需要把每个可处置的类型包装在try/catch/finally块中。为此,C#提供特殊的语法,如下所示:

static void Main()
{
//当退出using作用域时,自动调用Dispose()
using(MyResourceWrapper rw =
new MyResourceWrapper())
{
//使用rw对象
}
}

//如果用ildasm.exe查看Main()方法的CIL代码,会发现using语法确实用Dispose()调

//用扩展了try/finally逻辑:
.method private hidebysig
static void Main(string[] args) cil managed
{
...
.try
{
...
} // end .try
finally
{
...
IL_0012: callvirt instance void
SimpleFinalize.MyResourceWrapper::Dispose()
} // end handler
...
} // end of method Program::Main

构建可终结可处置类型

现在有两种方式来构造能够清理内部非托管资源的类,其一可以重写Finalize()方法,其二可以实现IDisposable。前者尽可以放心,因为对象可以不需要用户参与进行垃圾回收,清除它自身;后者给对象用户提供一种一旦对象用完就能清除的方法,但是如果用户忘记调用Dispose(),非托管资源可能会永远留在内存中。

同时采用上述两种技术是可行的,可以获得两种模型的好处,如果对象用户调用Dispose(),可以通过调用GC.SuppressFinalize()通知垃圾回收器跳过终结过程;如果忘记调用Dispose(),对象最终也将被终结。这样的做法确实能很好地工作,但还有小缺陷,首先Finalize()和Dispose()方法都有相同的清除非托管资源的代码,应该定义一个私有的辅助函数供两个方法调用。另外,还要确保Finalize()方法不会尝试处置任何托管对象,而Dispose()方法则应该这样做。最后还要确保对象用户可以安全地多次调用Dispose()而不出错误。

为了实现这样的设计,微软定义了一个正式的可终结可处置模式,它在健壮性、可维护性和性能三者之间取得了平衡。下面是这个模式的示例:

public class MyResourceWrapper : IDisposable
{
// Used to determine if Dispose() has already been called.

// 用来判断Dispose()是否已经被调用

private bool disposed =
false;

public void Dispose()
{
// Call our helper method.调用辅助方法

// Specifying "true" signifies that

// the object user triggered the clean up.

// 指定true表示对象用户触发了清理过程

Dispose(true);

// Now suppress finialization.

// 现在跳过终结
GC.SuppressFinalize(this);
}

//protected void virtual Dispose(bool disposing)

//也可以这样定义该方法签名
private
void Dispose(bool disposing)
{
// Be sure we have not already been disposed!

//确保还没有被处置
if (!this.disposed)
{
// If disposing equals true, dispose all

// managed resources.如果disposing等于true,处置所有托管资源

if (disposing)
{
// Dispose managed resources.处置托管的资源

}

// Clean up unmanaged resources here.在这里清理非托管的资源

}
disposed = true;
}

~MyResourceWrapper()
{
// Call our helper method.调用辅助方法

// Specifying "false" signifies that

// the GC triggered the clean up.

// 指定false表示GC触发了清理过程
Dispose(false);
}
}

总结

至此,CLR通过垃圾回收器管理对象的过程描述结束,当然不包括一些细节,如:弱引用(weak reference)和对象复苏(object resurrection)等。终结全文,可以总结出如下几条规则:
法则1. 使用new关键字将一个对象分配在托管堆上,然后就不用再管;
法则2. 如果托管堆没有足够的内存来分配所请求的对象,就会进行垃圾回收;
法则3. 重写Finalize()的唯一原因是,C#类通过PInvoke或复杂的COM互操作性任务使用了非托管资源(典型情况是通过System.Runtime.InteropServices.Marshal类型);
法则4. 如果对象支持IDisposable,总是要对任何直接创建的对象调用Dispose()。应该认为,如果类设计者选择支持Dispose()方法,这个类型就需要执行清除工作;
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: