使用WinDbg —— .NET篇 (五)

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

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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;

 

namespace TestGCHandle

{

    [StructLayout(LayoutKind.Sequential)]

    class A

    {

        public int _num;

        

        public A(int num)

        {

            _num = num;

        }

    }

 

    class Program

    {

        static void Main(string[] args)

        {

            Tuple<GCHandle, GCHandle, GCHandle> tuple = Alloc();

            Console.WriteLine("Allocated objects to GC Handle table. Press any key to perform GC...");

            Console.ReadKey();

 

            GC.Collect();

            GCHandle weakHanle = tuple.Item1;

            Console.WriteLine("The instance of A allocated in GC handle table with GCHandleType.Weak == null: {0}", weakHanle.Target == null);

            Console.WriteLine("GC completed. Press any key to free objects from GC handle table...");

            Console.ReadKey();

 

            tuple.Item2.Free();

            tuple.Item3.Free();

            Console.WriteLine("Free compled! Press any key to perform GC...");

            Console.ReadKey();

 

            GC.Collect(1);

            Console.WriteLine("GC completed.");

            Console.ReadKey();

 

        }

 

        static Tuple<GCHandle, GCHandle, GCHandle> Alloc()

        {

            Collection<byte[]> list = new Collection<byte[]>();

            // Forces the GC to compact memory after collecting garbage.

            for (int i = 0; i < 1024; i++)

            {

                list.Add(new byte[1024]);

            }

 

            A a1 = new A(1);

            A a2 = new A(2);

            A a3 = new A(3);

            GCHandle h1 = GCHandle.Alloc(a1, GCHandleType.Weak);

            GCHandle h2 = GCHandle.Alloc(a2, GCHandleType.Normal);

            GCHandle h3 = GCHandle.Alloc(a3, GCHandleType.Pinned);

            return new Tuple<GCHandle, GCHandle, GCHandle>(h1, h2, h3);

        }

    }

}

这段代码,分析了使用GCHandle的四个阶段:

1. 第一阶段声明类A的三个对象a1a2a3,分别以WeakNormalPinned的方式添加到GC句柄表,声明a1a2a3之前,我声明了1024个大小为1026字节的对象,这个目的是强制GC在回收垃圾后会对内存进行压缩(如果你对我之前讲的代的概念还有印象的话,应该知道当内存绰绰有余的时候,是不会进行内存压缩的)

2. 第二个阶段执行GC,并检测以Weak方式添加到GC句柄表的对象a1已经被回收了,即使我在后面引用这个对应的GCHandleTarget(如果a1对象还存在的话,可以通过Target属性获得a1的引用),这个时候a2a3对象变为了第一代。最后提一点,变量h1称为对象a1的弱引用。

3. 第三阶段我将a2a3GC Handle表移除掉。a1不需要移除掉,因为这时候a1已经被回收了。

4. 最后一阶段回收了第一代的中的垃圾,将a2a3回收掉。

每个阶段之后都调用了Console.ReadKey(),这样方便Windbg中断调试观察每个阶段的内存情况。现在利用Windbg启动目标程序,开始调试,让程序执行完第一阶段得到输出:

Allocated objects to GC Handle table. Press any key to perform GC...

然后在Windbg中中断程序,并执行“!GCHandles”命令:

0:006> !gchandles

  Handle Type          Object     Size     Data Type

008912f4 WeakShort   027b828c       12          TestGCHandle.A

008912fc WeakShort   027ba638       52          System.Threading.Thread

008910f8 WeakLong    026b2ba0       84          System.RuntimeType+RuntimeTypeCache

008911c8 Strong      027b9748       48          System.Object[]

008911cc Strong      027b8298       12          TestGCHandle.A

008911d4 Strong      026b1be4       36          System.Security.PermissionSet

008911d8 Strong      026b1238       28          System.SharedStatics

008911dc Strong      026b11c8       84          System.Threading.ThreadAbortException

008911e0 Strong      026b1174       84          System.Threading.ThreadAbortException

008911e4 Strong      026b1120       84          System.ExecutionEngineException

008911e8 Strong      026b10cc       84          System.StackOverflowException

008911ec Strong      026b1078       84          System.OutOfMemoryException

008911f0 Strong      026b1024       84          System.Exception

008911f8 Strong      027ba638       52          System.Threading.Thread

008911fc Strong      026b1370      112          System.AppDomain

008913e8 Pinned      027b82a4       12          TestGCHandle.A

008913ec Pinned      036b34c8     8172          System.Object[]

008913f0 Pinned      036b24b8     4092          System.Object[]

008913f4 Pinned      036b2298      524          System.Object[]

008913f8 Pinned      026b121c       12          System.Object

008913fc Pinned      036b1020     4708          System.Object[]

 

Statistics:

      MT    Count    TotalSize Class Name

6dcf299c        1           12 System.Object

6dcf2a38        1           28 System.SharedStatics

6dcf33fc        1           36 System.Security.PermissionSet

008b4d64        3           36 TestGCHandle.A

6dcfa068        1           84 System.RuntimeType+RuntimeTypeCache

6dcf2920        1           84 System.ExecutionEngineException

6dcf28dc        1           84 System.StackOverflowException

6dcf2898        1           84 System.OutOfMemoryException

6dcf2744        1           84 System.Exception

6dcf31ec        2          104 System.Threading.Thread

6dcf2ab4        1          112 System.AppDomain

6dcf2964        2          168 System.Threading.ThreadAbortException

6dcf29f0        5        17544 System.Object[]

Total 21 objects

 

Handles:

    Strong Handles:       12

    Pinned Handles:       6

    Weak Long Handles:    1

    Weak Short Handles:   2

在输出的信息中能看到类A的三个对象,在GC句柄表中分别以WeakShortStrongPinned的方式存在。这三种方式分别对应着枚举类型GCHandleTypeWeakNormalPinned。在堆中也能看到相应的这三个对象:

0:006> !dumpheap -type TestGCHandle.A

 Address       MT     Size

027b828c 008b4d64       12    

027b8298 008b4d64       12    

027b82a4 008b4d64       12    

 

Statistics:

      MT    Count    TotalSize Class Name

008b4d64        3           36 TestGCHandle.A

Total 3 objects

然后让程序继续执行,执行完第二阶段,中断回到Windbg 这时候控制台的输出信息是:

The instance of A allocated in GC handle table with GCHandleType.Weak == null: True

GC completed. Press any key to free objects from GC handle table...

同时在Windbg中查看GC句柄表的内容如下:

0:008> !gchandles

  Handle Type          Object     Size     Data Type

008912fc WeakShort   026b4b34       52          System.Threading.Thread

008911c8 Strong      026b3d30       48          System.Object[]

008911cc Strong      026b29c0       12          TestGCHandle.A

008911d4 Strong      026b1994       36          System.Security.PermissionSet

008911d8 Strong      026b1238       28          System.SharedStatics

008911dc Strong      026b11bc       84          System.Threading.ThreadAbortException

008911e0 Strong      026b1168       84          System.Threading.ThreadAbortException

008911e4 Strong      026b1114       84          System.ExecutionEngineException

008911e8 Strong      026b10c0       84          System.StackOverflowException

008911ec Strong      026b106c       84          System.OutOfMemoryException

008911f0 Strong      026b1018       84          System.Exception

008911f8 Strong      026b4b34       52          System.Threading.Thread

008911fc Strong      026b1370      112          System.AppDomain

008913e8 Pinned      027b82a4       12          TestGCHandle.A

008913ec Pinned      036b34c8     8172          System.Object[]

008913f0 Pinned      036b24b8     4092          System.Object[]

008913f4 Pinned      036b2298      524          System.Object[]

008913f8 Pinned      026b121c       12          System.Object

008913fc Pinned      036b1020     4708          System.Object[]

 

Statistics:

      MT    Count    TotalSize Class Name

6dcf299c        1           12 System.Object

008b4d64        2           24 TestGCHandle.A

6dcf2a38        1           28 System.SharedStatics

6dcf33fc        1           36 System.Security.PermissionSet

6dcf2920        1           84 System.ExecutionEngineException

6dcf28dc        1           84 System.StackOverflowException

6dcf2898        1           84 System.OutOfMemoryException

6dcf2744        1           84 System.Exception

6dcf31ec        2          104 System.Threading.Thread

6dcf2ab4        1          112 System.AppDomain

6dcf2964        2          168 System.Threading.ThreadAbortException

6dcf29f0        5        17544 System.Object[]

Total 19 objects

 

Handles:

    Strong Handles:       12

    Pinned Handles:       6

    Weak Short Handles:   1

这个输出信息可以看到以WeakShort方式存在的A的对象已经被回收掉了,也能从控制台的输出信息看到a1null;同时我们可以看到以Strong方式存在A的对象的地址已经变了,这是因为执行了内存压缩;然而以Pinned方式存在的A的对象地址不变。这个时候查看堆中的信息:

0:008> !dumpheap -type TestGCHandle.A

 Address       MT     Size

026b29c0 008b4d64       12    

027b82a4 008b4d64       12    

 

Statistics:

      MT    Count    TotalSize Class Name

008b4d64        2           24 TestGCHandle.A

Total 2 objects

Fragmented blocks larger than 0.5 MB:

    Addr     Size      Followed by

026b6bd0    1.0MB         027b82a4 TestGCHandle.A

这个输出信息非常好,可以看到我加粗的部分,提示了有超过0.5MB的碎片,这是因为对象a3被定住了,不允许被移动。我加粗的部分还包含了一个表格,这个表格的第一列Addr是指空闲碎片的地址;Size这一列表明有1.0MB大小的碎片;最后一列,标红的部分就是被定住的对象地址,同时后面还表明了这个被定住的对象类型。大家对比我标红的那个地址就是在GC句柄表中的以Pinned方式存在的A的对象,也就是a3

接着往下执行,执行完第三阶段,中断回到Windbg,这时候的输出信息是:

Free compled! Press any key to perform GC...

Windbg中,继续观察GC句柄表的信息:

0:008> !gchandles

  Handle Type          Object     Size     Data Type

008912fc WeakShort   026b4b34       52          System.Threading.Thread

008911c8 Strong      026b3d30       48          System.Object[]

008911d4 Strong      026b1994       36          System.Security.PermissionSet

008911d8 Strong      026b1238       28          System.SharedStatics

008911dc Strong      026b11bc       84          System.Threading.ThreadAbortException

008911e0 Strong      026b1168       84          System.Threading.ThreadAbortException

008911e4 Strong      026b1114       84          System.ExecutionEngineException

008911e8 Strong      026b10c0       84          System.StackOverflowException

008911ec Strong      026b106c       84          System.OutOfMemoryException

008911f0 Strong      026b1018       84          System.Exception

008911f8 Strong      026b4b34       52          System.Threading.Thread

008911fc Strong      026b1370      112          System.AppDomain

008913ec Pinned      036b34c8     8172          System.Object[]

008913f0 Pinned      036b24b8     4092          System.Object[]

008913f4 Pinned      036b2298      524          System.Object[]

008913f8 Pinned      026b121c       12          System.Object

008913fc Pinned      036b1020     4708          System.Object[]

 

Statistics:

      MT    Count    TotalSize Class Name

6dcf299c        1           12 System.Object

6dcf2a38        1           28 System.SharedStatics

6dcf33fc        1           36 System.Security.PermissionSet

6dcf2920        1           84 System.ExecutionEngineException

6dcf28dc        1           84 System.StackOverflowException

6dcf2898        1           84 System.OutOfMemoryException

6dcf2744        1           84 System.Exception

6dcf31ec        2          104 System.Threading.Thread

6dcf2ab4        1          112 System.AppDomain

6dcf2964        2          168 System.Threading.ThreadAbortException

6dcf29f0        5        17544 System.Object[]

Total 17 objects

 

Handles:

    Strong Handles:       11

    Pinned Handles:       5

    Weak Short Handles:   1

这个时候看到a2a3都从GC句柄表中移除掉了。然后查看堆中的信息:

0:008> !dumpheap -type TestGCHandle.A

 Address       MT     Size

026b29c0 008b4d64       12    

027b82a4 008b4d64       12    

 

Statistics:

      MT    Count    TotalSize Class Name

008b4d64        2           24 TestGCHandle.A

Total 2 objects

Fragmented blocks larger than 0.5 MB:

    Addr     Size      Followed by

026b6bd0    1.0MB         027b82a4 TestGCHandle.A

这个时候可以看到这A的两个对象a2a3还在内存中,依然有碎片的存在。

让程序继续执行,让GC回收第一代中的内存,得到输出信息:

GC completed.

这个时候查看堆中信息:

0:008> !dumpheap -type TestGCHandle.A

 Address       MT     Size

 

Statistics:

      MT    Count    TotalSize Class Name

Total 0 objects

这个时候A的所有对象才真正被回收掉了。通过这个例子能直观的看到GCHandle表中的对象对内存的影响。另外值得大家注意的是,对于NormalPinned方式添加的对象,需要我们手动写代码调用GCHandle实例的Free方法,GC不会帮我们调用,忘了调用或者调用的地方不会被执行到都会导致内存泄漏。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值