使用WinDbg —— .NET篇 (五)

本文详细介绍了.NET中终结方法(Finalize)的工作原理,包括终结列表和终结可达队列的角色。同时,探讨了GC句柄表,特别是GCHandle在对象生命周期管理中的应用,如Normal、Pinned方式的差异,并通过示例展示了对象如何在内存中受到句柄表影响的过程。
摘要由CSDN通过智能技术生成

6.2  终结方法

C++语言里面有个概念叫做析构函数析构函数的语法和C#的终结方法一样,都是通过“~ + 类名作为函数名。也有人把C#的终结方法叫做析构函数,然而我还是比较喜欢终结方法这种叫法,一者因为C#中的终结方法的执行机制跟C++的析构函数是完全不一样的,被称作析构函数容易混淆;二者因为终结方法编译后的模块中,生成的IL中能看到生成的方法名就叫做Finalize

之前我们看到执行GC的一个过程,然而对于实现了终结方法的类实例,回收过程有点不一样。 对于实现了终结方法的类实例都会保存在一个叫做终结列表里面(FinalizationList),然后还有一个用于GC处理终结方法对象的结构叫做终结可达队列(F-reachableQueue)。创建初始化一个终结方法类的实例后,会把这个对象添加到终结列表里面,当这个对象变为了不可达对象,执行GC检测回收内存的时候,会将这个对象从终结列表里面移除掉,同时将这对象添加到终结可达队列中,然后这个对象就不算是不可达对象,GC把这个对象标为可达对象,然后有一个独立的线程专门检测这个终结可达队列,把对象从这个队列里面移除,并执行这个对象的终结方法,然后等下一次执行GC的时候,判断出这个对象是不可达的,而且即不在终结列表中也不在终结可达队列中,这个时候这个对象才会真正被回收掉。

为了避免误解,在这里纠正我的一个说法:

前面说的实现了终结方法的类,这个说法其实不太准确,准确的说法是重写了终结方法的类实例。因为超级基类System.Object是实现了终结方法的,继承了Object而没有重写Finalize方法的类的实例是不会放在终结列表里面,而重写Finalize方法的方式是通过析构函数的语法,有意思的是,当你手动的去重写Finalize方法会在编译时会得到一个错误。查看Object类的源码可以看到Finalize方法声明如下:

protected virtual void Finalize()
{

}

SOS中有个专门用于查找终结列表中的数据的命令:

0:006> !FinalizeQueue

SyncBlocks to be cleaned up: 0

Free-Threaded Interfaces to be released: 0

MTA Interfaces to be released: 0

STA Interfaces to be released: 0

----------------------------------

generation 0 has 6 finalizable objects (00b0a750->00b0a768)

generation 1 has 0 finalizable objects (00b0a750->00b0a750)

generation 2 has 0 finalizable objects (00b0a750->00b0a750)

Ready for finalization 0 objects (00b0a768->00b0a768)

Statistics for all finalizable objects (including all objects ready for finalization):

      MT    Count    TotalSize Class Name

00ad4d50        1           12 TestFinalizer.A

657b6048        1           20 Microsoft.Win32.SafeHandles.SafeFileHandle

657b3544        1           20 Microsoft.Win32.SafeHandles.SafePEFileHandle

657a4708        1           20 Microsoft.Win32.SafeHandles.SafeFileMappingHandle

657a46b8        1           20 Microsoft.Win32.SafeHandles.SafeViewOfFileHandle

657b4e90        1           44 System.Threading.ReaderWriterLock

Total 6 objects

打印出来的信息里面显示了各个代中包含的实现了终结方法对象的个数和相关的简要信息。

写一个Main方法里面为空的控制台程序,然后用Windbg调试,可以看到这个程序里面有两个线程:

可以看到其中0号线程是主线程,也就是执行了Main方法的那个线程,5号线程在Exception那一列有写着(Finalizer),这个线程就是前面说的用来检索遍历终结可达队列的线程,主要用来执行终结方法和从终结可达列表中移除执行过终结方法的对象。

6.3  GC句柄表

针对每个Domain,都维护着一张GC句柄表(GC Handle Table),这张表可以用来控制和监控对象的生命周期,这种控制一般用于与非托管代码交互的时候或者管理资源释放的时候。在命名空间System.Runtime.InteropServices下的GCHandle结构可以将对象添加到GC句柄表中,添加到句柄表中的对象能对其进行生命周期的管理或者监控。GCHandle结构中有个Alloc的静态方法,签名如下:

public static GCHandle Alloc(object value, GCHandleType type);

利用这个方法可以将对象以一定的方式加入GC句柄表里面,其中GCHandleType是一个枚举类型,其定义为:

public enum GCHandleType

{

    Weak = 0,

    WeakTrackResurrection = 1,

    Normal = 2,

    Pinned = 3

}

当对象是以Normal或者Pinned的方式添加到GC句柄表时,如果这个对象在GC检测的时候被检测为不可达,之后GC会遍历句柄表,发现这个对象被注册为NormalPinnedGC会对这个对象进行标记,表示可达。也就是说当对象以Normal或者Pinned的方式分配在GC句柄表时会被阻止回收。我稍微介绍一下这个功能的其中一个使用场景:当我们使用了非托管代码交互的时候,有个对象会在非托管环境回到托管环境的时候被调用,但是在执行非托管代码的时候没有任何的根指向这个对象,因此导致这个对象不可达,如果这个对象在执行非托管对象的时候被GC回收了,那么等从非托管代码回到托管环境的时候调用这个对象就会找不到这个对象。了解了使用场景后,不禁要问到NormalPinned的区别,其实在前面讲代的概念时候提到了Pinned,这个区别就是在GC执行回收之后决定这个对象在压缩的时候要不要移动,Normal方式添加的会被移动,Pinned方式添加的不会。Pinned方式这个作用可以用在将托管对象传到非托管代码,而且非托管代码可以使用这个对象,调用这个对象里面的值,试想一下,如果在GC回收的时候,移动了这个对象,非托管代码就会定位到一个错误的地址,为了避免这种情况,可以使用Pinned的方式定住对象,这就相当于告诉了GC既不要回收这个对象也不要移动这个对象。等从非托管代码中回到托管代码的时候需要调用这个结构实例的Free方法将对象从GC句柄表中移除掉。GCHandleType中还有一个WeakWeakTrackResurrection,其中Weak的用法比较简单,在这里不描述了,后面有个例子演示了Weak的用法;使用WeakTrackResurrection的方式比较少,所以这个不讲,有兴趣可以参考Jeffery的《CLR via C#》。

同样,在SOS中有专门查找GC句柄表中的对象的命令,在看GC Handle相关命令,先看段代码:

using System;

using System.Collections.ObjectModel;

using System.Runtime.InteropServices;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值