杨存伟
(西南大学 计算机科学与技术系,重庆 402460)
在.NET中,内存资源分为托管资源和非托管资源,其中托管资源指的是.NET 可以自动进行回收的资源,主要是指托管堆上分配的内存资源[1]。GC 类中包含了垃圾回收相关的方法,其中GC.KeepAlive 是其中一个较为特别的方法,它利用编译器和运行时的特性,阻止对象过早被回收。
示例1
在类Value中,实现了IDisposable中的Dispose方法,在类OuterClass 中,析构方法调用类成员i 的Dispose 方法来释放资源。
在Main 函数中,生成了一个Outerclass 的对象,变量名为outer,并将outer.i 作为参数传递给Do函数。outer.i 作为参数传入之后,不存在对Outerclass 的对象的使用,因此垃圾回收器认为该对象已经无用,而outer.i 是有效的。从这时起到程序运行结束的任何时刻,垃圾回收器都有可能执行一次回收(回收Outerclass 的对象)。在垃圾回收器调用outer 的析构方法后,outer.i 已经执行了Dispose 方法,从而Do 函数中对outer.i 的操作可能是无效的,造成程序出错(图1)。
图1 不调用GC.KeepAlive 方法时程序的执行流程
若在Do(outer.i)后添加“GC.KeepAlive(outer)”,则Do 方法调用之后依然有对outer 的使用,保证了在调用Do 方法的时候,OuterClass 的对象不会被垃圾回收器回收(图2)[2]。
图2 调用GC.KeepAlive 方法时程序的执行流程
Microsoft 对该方法的官方实现为:
属性(Attribute)是用于在运行时传递程序中各种元素(类、结构体、方法等)的行为信息的声明性质的标签,它添加元数据,如编译器指令和注释、描述、方法、类等其他信息。
.NET 提供了两种类型的特性:预定义特性和自定义特性。MethodImplAttribute 和Reliability-Contract 都属于预定义特性[3]。
MethodImplAttribute 属性指定了一个方法是怎样编译和执行的。
ImplOptions.NoInlining 指定了该方法不应该被内联,由于GC.KeepAlive 并没有对传入的对象进行任何操作(也不应该进行任何操作),假如将其内联,将不能保证在调用该方法之前传入的对象不被垃圾回收。
ReliabilityContract 属性与实现原理无关,与安全性和可靠性有关,因此不做解析。
.NET 的垃圾回收器使用一种引用跟踪算法。在垃圾回收时,暂停所有线程,遍历所有堆内存中存在的对象,若引用类型变量引用了某个对象,该对象的同步索引字段将被置为1,否则为0。完成遍历后,同步索引字段为0 的对象是可以被回收的(不一定会被回收)[4]。
在示例1 中,outer.i 作为参数传入后,假设垃圾回收器开始回收,将发现OuterClass 的对象的同步索引块标记为0,这意味着没有变量继续引用OuterClass 的对象,则其可以回收。
若加上“GC.KeepAlive(outer)”,则outer.i 作为参数传入后,假如垃圾回收器开始回收,发现在Main 函数中还有待调用的GC.KeepAlive 方法使用了OuterClass 的对象,因此在调用GC.KeepAlive 之前,同步索引块都为1,即该对象都必须存在,不应该被回收。假设将“GC.KeepAlive(outer)”替换成“var str=outer.ToString ()” 等语句(只要使用了OuterClass 的对象),和调用GC.KeepAlive 方法的作用是一样的,只是该方法不产生任何副作用。
示例2:
.NET 的垃圾回收器查找未被引用的对象时,使用可达性分析算法[5],WeakReference 引用变量不会存在于GCRoots 开始的引用链中。若在调用GetGenerationWR 方法之后不调用KeepAlive 方法,不能保证wo 所指向的对象一定是存在的,造成调用失败。
示例3:
在将certificate.Handle 作为参数传入CertDuplicateCertificateContext 方法后,若没有GC.KeepAlive 方法,将导致certificate 随时被垃圾回收,导致certificate.Handle 失效。
示例4:
PrepareMethod 方法是对外部函数_PrepareMethod 的一层包装,在CopyRuntimeTypeHandles 方法使用instantiation 作为参数后,instantiationHandles 的有效性依然依赖于instantiation,故在_PrepareMethod 函数后调用GC.KeepAlive 方法来保证instantiation 在PrepareMethod 函数返回前都是有效的。
GC.KeepAlive 的实现并未调用.NET 中的内部类和方法,也未调用Win32 的API,其实质是运用了编译器和运行时的特性,保证了对象不被过早回收,其实现十分简洁。在实际运用中准确运用此方法,提高了程序的安全性和稳定性。