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会遍历句柄表,发现这个对象被注册为Normal或Pinned,GC会对这个对象进行标记,表示可达。也就是说当对象以Normal或者Pinned的方式分配在GC句柄表时会被阻止回收。我稍微介绍一下这个功能的其中一个使用场景:当我们使用了非托管代码交互的时候,有个对象会在非托管环境回到托管环境的时候被调用,但是在执行非托管代码的时候没有任何的根指向这个对象,因此导致这个对象不可达,如果这个对象在执行非托管对象的时候被GC回收了,那么等从非托管代码回到托管环境的时候调用这个对象就会找不到这个对象。了解了使用场景后,不禁要问到Normal和Pinned的区别,其实在前面讲代的概念时候提到了Pinned,这个区别就是在GC执行回收之后决定这个对象在压缩的时候要不要移动,Normal方式添加的会被移动,Pinned方式添加的不会。Pinned方式这个作用可以用在将托管对象传到非托管代码,而且非托管代码可以使用这个对象,调用这个对象里面的值,试想一下,如果在GC回收的时候,移动了这个对象,非托管代码就会定位到一个错误的地址,为了避免这种情况,可以使用Pinned的方式定住对象,这就相当于告诉了GC既不要回收这个对象也不要移动这个对象。等从非托管代码中回到托管代码的时候需要调用这个结构实例的Free方法将对象从GC句柄表中移除掉。GCHandleType中还有一个Weak和WeakTrackResurrection,其中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的三个对象a1,a2,a3,分别以Weak,Normal,Pinned的方式添加到GC句柄表,声明a1,a2,a3之前,我声明了1024个大小为1026字节的对象,这个目的是强制GC在回收垃圾后会对内存进行压缩(如果你对我之前讲的代的概念还有印象的话,应该知道当内存绰绰有余的时候,是不会进行内存压缩的)。
2. 第二个阶段执行GC,并检测以Weak方式添加到GC句柄表的对象a1已经被回收了,即使我在后面引用这个对应的GCHandle的Target(如果a1对象还存在的话,可以通过Target属性获得a1的引用),这个时候a2,a3对象变为了第一代。最后提一点,变量h1称为对象a1的弱引用。
3. 第三阶段我将a2,a3从GC Handle表移除掉。a1不需要移除掉,因为这时候a1已经被回收了。
4. 最后一阶段回收了第一代的中的垃圾,将a2,a3回收掉。
每个阶段之后都调用了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句柄表中分别以WeakShort,Strong,Pinned的方式存在。这三种方式分别对应着枚举类型GCHandleType的Weak,Normal和Pinned。在堆中也能看到相应的这三个对象:
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的对象已经被回收掉了,也能从控制台的输出信息看到a1为null;同时我们可以看到以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 |
这个时候看到a2,a3都从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的两个对象a2,a3还在内存中,依然有碎片的存在。
让程序继续执行,让GC回收第一代中的内存,得到输出信息:
GC completed. |
这个时候查看堆中信息:
0:008> !dumpheap -type TestGCHandle.A Address MT Size
Statistics: MT Count TotalSize Class Name Total 0 objects |
这个时候A的所有对象才真正被回收掉了。通过这个例子能直观的看到GCHandle表中的对象对内存的影响。另外值得大家注意的是,对于Normal和Pinned方式添加的对象,需要我们手动写代码调用GCHandle实例的Free方法,GC不会帮我们调用,忘了调用或者调用的地方不会被执行到都会导致内存泄漏。