http://www.blogcn.com/user8/flier_lu/index.html?id=2231935&run=.0210194
在了解了 Finalization 存在的问题后,接下来看看 CLR 1.0 和 1.1 中的现状,以及 Whidbey (v2.0) 中是如何尝试解决这些问题的。
在 v1.0 和 v1.1 中,一旦创建一个 finalizable 对象,则此对象被加入到 RegisteredForFinalization 系统队列中,此对象结束前可能出现一下的情况
1.完成正常的 Finalization 操作流程。对象顺利地被 Finalizer 线程调用 Finalize 方法,并最后被 GC 回收。这是最理想的状况 :P
2.不经过 CLR 关闭流程。这种情况发生在进程被通过 TerminateProcess 或 ExitProcess 函数直接停止了,CLR 只是通过 DllMain 函数的 DLL_PROCESS_DETACH 消息获知进程终止,而此时又不能调用 Managed 代码,所以 Finalize 方法不会被调用,只能依赖于操作系统在进程级对资源的回收。所以内存和句柄引用的内核对象将被正确回收,但保存在内存中的缓冲区等“软”资源会丢失。
3.经过 CLR 关闭流程。这种情况发生在进程通过 System.Environment.Exit 方法等 Managed 方法停止,CLR 会被引发相应的处理函数,完成所以线程的终止操作,以及 RegisteredForFinalization 和 ReadyToFinalize 队列的处理工作。这种情况也可以接受。
System.Environment.Exit 方法 (bclsystemEnvironment.cs:83) 调用 System.Environment.ExitNative 外部方法,通过 CLR 在 SystemNative::Exit 函数实现 (vmcomsystem.cpp:833) 中调用 ForceEEShutdown 函数 (vmceemain.cpp:658),强制调用 EEShutDown 函数 (vmceemain.cpp:739) 完成 CLR 执行环境的关闭流程。EEShutDown 函数中分两步调用 GCHeap::FinalizerThreadWatchDog (vmgcee.cpp:975) 完成 Finalization 流程。
4.经过 GC 流程强制被回收。这种情况通常发生在对象类型所在 AppDomain 被 Unload 时,AppDomain 只有在其所有类型的对象所在 RegisteredForFinalization 和 ReadyToFinalize 队列的处理工作完成之后,才会被实际卸载。
System.AppDomain.Unload 方法 (bclsystemAppDomain.cs:696) 调用 System.AppDomain.UnloadWorker.Unload 方法 (bclsystemAppDomain.cs:1702) 间接通过 System.AppDomain.UnloadThreadWorker.Unload 方法 (bclsystemAppDomain.cs:1746) 创建一个后台线程完成实际的 AppDomain 卸载工作。而在 AppDomainNative::Unload 函数 (vmappdomainnative.cpp:471) 中实现的 AppDomain.nUnload 完成真正的卸载工作。而 AppDomainNative::Unload 函数内部调用 AppDomain::Exit 函数 (vmappdomain.cpp:5623) 完成类型卸载、资源回收,以及在调用 GCHeap::GarbageCollect 函数 (vmgcee.cpp:5351) 回收被卸载类型对象后,调用 GCHeap::FinalizerThreadWait 函数 (vmgcee.cpp:934) 释放 finalizable 对象。
此外 cbrumme 还提到了一些细节上需要注意的地方,例如除了 System.Threading.Thread 类的对象外,所有对象都是在其创建的 AppDomain 中被析构,而 Thread 对象在进程终止的过程中不被析构等等。因为这涉及到太多相关内容,这儿就不详细展开解释了,呵呵,每一条都足够单独写篇文章分析了 :P
如果有进一步了解的兴趣,可以参考 cbrumme 的另一篇文章 Startup, Shutdown & related matters。
在提出并分析问题后,cbrumme 给出了 Whidbey 中解决问题的一些思路和方法。
cbrumme 首先列举了一个 v2.0 中新增的 System.Runtime.InteropServices.SafeHandle 类型以及其种种优点。简单来说,这是一个操作安全的对 Win32 句柄进行包装的类,v2.0 的 BCL 从此类中派生出很多对句柄进行维护的类型,如 Win32SafeHandle 以及 Microsoft.Win32.SafeHandles 名字空间下一堆句柄包装类。这些类能够一定程度上解决构造和析构过程原子化等问题,杜绝诸如句柄重用攻击和资源耗尽攻击。而这一神奇类型真正的幕后功臣是 Critical Finalization 概念的出现和应用。
任一从 System.Runtime.Reliability.CriticalFinalizerObject 类型继承的子类,都自动获得 Critical Finalization (CF)的能力,CLR v2.0 将保障这些类型满足一下需求:
1.此类对象构造之前,CLR 将预先准备好调用此对象 Finalize 方法所需的各种资源。这种准备包括预先 JIT 代码,允许类构造函数 (class constructor),以及静态可达的所以其他类型。虽然如前面所述,这种静态分析无法处理通过虚函数或接口间接引用的对象,那些问题需要程序员来解决。
2.CLR 在执行这些类型的 Finalize 函数时不会超时。这就要求此类函数的 Finalze 代码编写非常小心,值得信任,呵呵
3.调用 Finalize 函数时,CLR 会进入一种保护状态,防止因为异步异常中断执行操作。例如 JIT 的 OutOfMemoryExceptions 或调用 .cctors 的 TypeInitializationExceptions 异常将被暂时屏蔽,保障 Finalize 函数运行的原子性。不过应该程序原因导致的异常,如分配对象导致 OutOfMemoryException 异常,这属于程序自身的问题,可以通过 try...catch 保护,不在系统保护范畴之内。
4.所有其它的普通对象在此类对象之前被析构,无论析构成功与否,都能保障在此类对象执行析构操作时,它们已经完成工作。
实际上 CF 的前三点跟 Constrained Execution Regions (CER) 要解决的问题是一致的,也就是对异步异常的处理模型。cbrumme 在另一篇文章 Reliability 中详细讨论了这方面问题,这里就不罗嗦了。而 v2.0 BCL 中 System.Runtime.Reliability 名字空间下增加的几个类型,就是为了解决这方面问题,可惜介绍的资料太少了。
而对于 CF 来说,在普通的程序中其重要意义并不能很好体现出来,毕竟碰到内存耗尽和调用 Thread.Abort 的机会并不多。不过在某些受限情况下,如 SQL Server (Yukon) 中,这种问题的解决就具有重要意义。例如作为 CLR 宿主的 Yukon 经常运行在临界资源状态下,异步 异常和强制用 Thread.Abort 中断时间过长的查询都是经常碰到的。好在 Whidbey 将提供基于策略的执行机制,AppDomain 卸载、普通的中断操作等等都会加入超时策略机制,通过配置来缓解部分问题。
看到这里,偶对 cbrumme 的仰慕之情如滔滔江水连绵不决,又有如黄河泛滥一
发而不可收拾...呵呵
在了解了 Finalization 存在的问题后,接下来看看 CLR 1.0 和 1.1 中的现状,以及 Whidbey (v2.0) 中是如何尝试解决这些问题的。
在 v1.0 和 v1.1 中,一旦创建一个 finalizable 对象,则此对象被加入到 RegisteredForFinalization 系统队列中,此对象结束前可能出现一下的情况
1.完成正常的 Finalization 操作流程。对象顺利地被 Finalizer 线程调用 Finalize 方法,并最后被 GC 回收。这是最理想的状况 :P
2.不经过 CLR 关闭流程。这种情况发生在进程被通过 TerminateProcess 或 ExitProcess 函数直接停止了,CLR 只是通过 DllMain 函数的 DLL_PROCESS_DETACH 消息获知进程终止,而此时又不能调用 Managed 代码,所以 Finalize 方法不会被调用,只能依赖于操作系统在进程级对资源的回收。所以内存和句柄引用的内核对象将被正确回收,但保存在内存中的缓冲区等“软”资源会丢失。
3.经过 CLR 关闭流程。这种情况发生在进程通过 System.Environment.Exit 方法等 Managed 方法停止,CLR 会被引发相应的处理函数,完成所以线程的终止操作,以及 RegisteredForFinalization 和 ReadyToFinalize 队列的处理工作。这种情况也可以接受。
System.Environment.Exit 方法 (bclsystemEnvironment.cs:83) 调用 System.Environment.ExitNative 外部方法,通过 CLR 在 SystemNative::Exit 函数实现 (vmcomsystem.cpp:833) 中调用 ForceEEShutdown 函数 (vmceemain.cpp:658),强制调用 EEShutDown 函数 (vmceemain.cpp:739) 完成 CLR 执行环境的关闭流程。EEShutDown 函数中分两步调用 GCHeap::FinalizerThreadWatchDog (vmgcee.cpp:975) 完成 Finalization 流程。
4.经过 GC 流程强制被回收。这种情况通常发生在对象类型所在 AppDomain 被 Unload 时,AppDomain 只有在其所有类型的对象所在 RegisteredForFinalization 和 ReadyToFinalize 队列的处理工作完成之后,才会被实际卸载。
System.AppDomain.Unload 方法 (bclsystemAppDomain.cs:696) 调用 System.AppDomain.UnloadWorker.Unload 方法 (bclsystemAppDomain.cs:1702) 间接通过 System.AppDomain.UnloadThreadWorker.Unload 方法 (bclsystemAppDomain.cs:1746) 创建一个后台线程完成实际的 AppDomain 卸载工作。而在 AppDomainNative::Unload 函数 (vmappdomainnative.cpp:471) 中实现的 AppDomain.nUnload 完成真正的卸载工作。而 AppDomainNative::Unload 函数内部调用 AppDomain::Exit 函数 (vmappdomain.cpp:5623) 完成类型卸载、资源回收,以及在调用 GCHeap::GarbageCollect 函数 (vmgcee.cpp:5351) 回收被卸载类型对象后,调用 GCHeap::FinalizerThreadWait 函数 (vmgcee.cpp:934) 释放 finalizable 对象。
此外 cbrumme 还提到了一些细节上需要注意的地方,例如除了 System.Threading.Thread 类的对象外,所有对象都是在其创建的 AppDomain 中被析构,而 Thread 对象在进程终止的过程中不被析构等等。因为这涉及到太多相关内容,这儿就不详细展开解释了,呵呵,每一条都足够单独写篇文章分析了 :P
如果有进一步了解的兴趣,可以参考 cbrumme 的另一篇文章 Startup, Shutdown & related matters。
在提出并分析问题后,cbrumme 给出了 Whidbey 中解决问题的一些思路和方法。
cbrumme 首先列举了一个 v2.0 中新增的 System.Runtime.InteropServices.SafeHandle 类型以及其种种优点。简单来说,这是一个操作安全的对 Win32 句柄进行包装的类,v2.0 的 BCL 从此类中派生出很多对句柄进行维护的类型,如 Win32SafeHandle 以及 Microsoft.Win32.SafeHandles 名字空间下一堆句柄包装类。这些类能够一定程度上解决构造和析构过程原子化等问题,杜绝诸如句柄重用攻击和资源耗尽攻击。而这一神奇类型真正的幕后功臣是 Critical Finalization 概念的出现和应用。
任一从 System.Runtime.Reliability.CriticalFinalizerObject 类型继承的子类,都自动获得 Critical Finalization (CF)的能力,CLR v2.0 将保障这些类型满足一下需求:
1.此类对象构造之前,CLR 将预先准备好调用此对象 Finalize 方法所需的各种资源。这种准备包括预先 JIT 代码,允许类构造函数 (class constructor),以及静态可达的所以其他类型。虽然如前面所述,这种静态分析无法处理通过虚函数或接口间接引用的对象,那些问题需要程序员来解决。
2.CLR 在执行这些类型的 Finalize 函数时不会超时。这就要求此类函数的 Finalze 代码编写非常小心,值得信任,呵呵
3.调用 Finalize 函数时,CLR 会进入一种保护状态,防止因为异步异常中断执行操作。例如 JIT 的 OutOfMemoryExceptions 或调用 .cctors 的 TypeInitializationExceptions 异常将被暂时屏蔽,保障 Finalize 函数运行的原子性。不过应该程序原因导致的异常,如分配对象导致 OutOfMemoryException 异常,这属于程序自身的问题,可以通过 try...catch 保护,不在系统保护范畴之内。
4.所有其它的普通对象在此类对象之前被析构,无论析构成功与否,都能保障在此类对象执行析构操作时,它们已经完成工作。
实际上 CF 的前三点跟 Constrained Execution Regions (CER) 要解决的问题是一致的,也就是对异步异常的处理模型。cbrumme 在另一篇文章 Reliability 中详细讨论了这方面问题,这里就不罗嗦了。而 v2.0 BCL 中 System.Runtime.Reliability 名字空间下增加的几个类型,就是为了解决这方面问题,可惜介绍的资料太少了。
而对于 CF 来说,在普通的程序中其重要意义并不能很好体现出来,毕竟碰到内存耗尽和调用 Thread.Abort 的机会并不多。不过在某些受限情况下,如 SQL Server (Yukon) 中,这种问题的解决就具有重要意义。例如作为 CLR 宿主的 Yukon 经常运行在临界资源状态下,异步 异常和强制用 Thread.Abort 中断时间过长的查询都是经常碰到的。好在 Whidbey 将提供基于策略的执行机制,AppDomain 卸载、普通的中断操作等等都会加入超时策略机制,通过配置来缓解部分问题。
看到这里,偶对 cbrumme 的仰慕之情如滔滔江水连绵不决,又有如黄河泛滥一
发而不可收拾...呵呵