《CLR via C#》笔记——AppDomain(2)
2012-07-17 09:22 xiashengwang 阅读(2233) 评论(5) 编辑 收藏 举报四,卸载AppDomain
AppDomain很出色的一个能力就是它允许卸载。卸载AppDomain会导致CLR卸载AppDomain中的所有程序集,还会释放AppDomain的Loader堆。为了卸载一个AppDomain,可以调用AppDomain的静态方法UnLoad,这将导致CLR执行一系列的操作来卸载AppDomain。
1,CLR挂起进程中执行过托管代码的所有线程。
2,CLR检查所有线程栈,查看哪些线程正在执行要卸载的那个AppDomain中的代码,或者哪些线程会在某个时候返回至要卸载的那个AppDomain。在任何一个栈上,如果有准备卸载的AppDomain,CLR都会强迫对应的线程抛出一个ThreadAbortException异常(同时恢复线程的执行)。这将导致线程展开(unwind),在展开的过程中执行遇到的所有finally块中的内容,以执行资源清理代码。如果没有代码捕获ThreadAbortException,它将最终成为一个未处理的异常,CLR会“吞噬”这个异常,线程会终止,但进程可以继续执行。这一点非常特别,因为对于其他所有未处理的异常,CLR都会终止进程。
3,当第2步发现的所有线程都离开AppDomain后,CLR遍历堆,为引用了“由已卸载AppDomain创建的对象”的每一个代理对象设置一个标志(flag)。这些代理对象现在知道它们引用的真是对象已经不存在了。如果任何代码在无效的代理对象上调用一个方法,该方法会抛出一个AppDomainUnloadedException。
4,CLR强制垃圾回收,对现有已卸载AppDomain创建的任何对象占用的内存进行回收。这些对象的Finalize方法被调用,使对象有机会彻底清理它们占用的资源。
5,CLR恢复剩余线程的执行。调用AppDomain.Unload方法的线程将继续运行,对AppDomain.Unload的调用是同步的。
顺便说一句,当一个线程调用AppDomain.Unload方法时,针对要卸载的AppDomain中的线程,CLR会给它们10秒钟的时间离开。10秒钟后,如果调用AppDomain.Unload方法的线程还没有返回,CLR将抛出一个CannotUnloadAppDomainException异常,AppDomain将来可能会,也可以能不会卸载。
如果调用AppDomain.Unload的线程不巧在要卸载的AppDomain中,CLR会创建一个新的线程来尝试卸载AppDomain。第一个线程被强制抛出一个ThreadAbortException异常并展开(unwind),新建的线程将等待AppDomain的卸载,然后线程会终止。如果AppDomain卸载失败,新线程将处理CannotUnloadAppDomainException异常。
五,监视AppDomain
宿主应用程序可监视AppDomain消耗的资源。有的宿主根据这种信息判断一个AppDomain的内存或CPU是否超过了应有的水准,并强制卸载一个AppDomain。还可以用监视来比较不同算法的资源消耗情况,判断哪一种算法用的资源较少。AppDomain监视本身也会产生开销,要打开监视需要显示的将AppDomain的静态属性MonitoringEnabled设为true。一旦打开监视便不能关闭,如果试图将MonitoringEnabled改为false,会抛出一个ArgumentException异常。
监视打开后,你的代码可以查询AppDomain类提供的一下4个只读属性:
1,MonitoringSurvivedProcessMemorySize 这个Int64的静态属性返回当前CLR所有AppDomain正在使用的字节数。这个数值保证在上次垃圾回收时是正确的。
2,MonitoringTotalAllocatedMemorySize 这个Int64的实例属性返回一个特定的AppDomain已分配的字节数。这个数值保证在上次垃圾回收时是正确的。
3,MonitoringSurvivedMemorySize 这个Int64的实例属性返回返回一个特定的AppDomain当前正在使用的字节数。这个数值保证在上次垃圾回收时是正确的。
4,MonitoringTotalProcessorTime 这个TimeSpan实例属性返回一个特定的AppDomain的CPU占用率。
下面这个类演示了在两个时间点之间,AppDomain中发生的变化:
public sealed class AppDomainMonitorDelta : IDisposable { private AppDomain m_appDomain; private TimeSpan m_thisADCpu; private Int64 m_thisMemoryInUse; private Int64 m_thisMemoryAllocated; static AppDomainMonitorDelta() { //启用p监视 AppDomain.MonitoringIsEnabled = true; } public AppDomainMonitorDelta(AppDomain appDomain) { m_appDomain = appDomain ?? AppDomain.CurrentDomain; m_thisMemoryAllocated = m_appDomain.MonitoringTotalAllocatedMemorySize; m_thisMemoryInUse = m_appDomain.MonitoringSurvivedMemorySize; m_thisADCpu = m_appDomain.MonitoringTotalProcessorTime; } #region IDisposable public void Dispose() { GC.Collect(); Console.WriteLine("FriendlyName={0},CPU={1}ms", m_appDomain.FriendlyName, (m_appDomain.MonitoringTotalProcessorTime - m_thisADCpu).TotalMilliseconds); Console.WriteLine("Allocated {0:N0} bytes of which {1:N0} survived GCs.", m_appDomain.MonitoringTotalAllocatedMemorySize - m_thisMemoryAllocated, m_appDomain.MonitoringSurvivedMemorySize - m_thisMemoryInUse); } #endregion }
下面的代码调用了AppDomainMonitorDelta
public static void AppDomainResourceMonitor() { using (new AppDomainMonitorDelta(null)) { //分配后可以存活的10M var list = new List<object>(); for (int x = 0; x < 1000; x++) { list.Add(new byte[10000]); } //分配后不能存活的20M for (int x = 0; x < 2000; x++) { new byte[10000].GetType(); } //保持cpu工作5秒 Int64 stop = Environment.TickCount + 5000; while (Environment.TickCount < stop) ; } }
运行的结果:
FriendlyName=AppDomainText.vshost.exe,CPU=5015.625ms Allocated 30,102,936 bytes of which 10,121,606 survived GCs.
六,AppDomain FirstChance异常通知
AppDomain可以关联一组回调方法,当程序中异常发生时,CLR开始查找AppDomain的中的catch块之前,这组回调方法预先被调用,这些方法可以执行日志的记录操作。宿主可以利用这个机制监视AppDomain中抛出的异常。但回调方法不能处理异常,也不能以任何方式“吞噬”它,它们只是接收关于异常发生的一个通知,要登记一个回调方法,只需要在AppDomain的实例事件FirstChanceException添加一个委托即可。
public AppDomainMonitorDelta(AppDomain appDomain) { m_appDomain = appDomain ?? AppDomain.CurrentDomain; m_thisMemoryAllocated = m_appDomain.MonitoringTotalAllocatedMemorySize; m_thisMemoryInUse = m_appDomain.MonitoringSurvivedMemorySize; m_thisADCpu = m_appDomain.MonitoringTotalProcessorTime; m_appDomain.FirstChanceException += new EventHandler<System.Runtime.ExceptionServices.FirstChanceExceptionEventArgs>(m_appDomain_FirstChanceException); try { throw new ArgumentException(); } catch { //先调用m_appDomain_FirstChanceException,然后才会进到这里 } try { throw new OutOfMemoryException(); } catch { //先调用m_appDomain_FirstChanceException,然后才会进到这里 } } void m_appDomain_FirstChanceException(object sender, System.Runtime.ExceptionServices.FirstChanceExceptionEventArgs e) { //在这里我们可以记录日志等信息 Console.WriteLine("m_appDomain_FirstChanceException:" + e.Exception.ToString()); }
下面描述CLR如何处理一个异常:异常首次抛出时,CLR会调用以抛出异常的那个AppDomain登记的任何FirstChanceException回调方法。然后CLR查找栈上在同一个AppDomain中的任何catch块。如果有一个catch块处理异常,则异常处理完成,将继续执行。如果AppDomain中没有个catch块处理异常,则CLR沿着栈向上来到调用AppDomain的地方(这里的调用AppDomain,有点让人误解,其实就是调用出异常的那句代码的位置),再次抛出同一个异常。这个时候感觉就像是抛出了一个全新的异常,CLR会调用已向当前AppDomain登记的任何FirstChanceException回调方法。这个过程会一直递归下去,直到抵达线程栈的顶部。到那时,如果异常还未被任何代码处理,CLR将终止整个进程。
顺便提一句,如果要应用程序异常终止前捕获AppDomain中未处理的异常,可以订阅UnhandledException事件。在这个事件中可以捕获异常,但随后程序依然会异常终止。在这个事件中同样只能得到事件的通知,并不能处理异常。所以一般的情况下,记录完异常信息后可以显示强制终止应用程序(Environment.Exit(1))。另外一个值得注意异常事件是Application.ThreadException,这是个在Winform程序中特有的事件,主要捕获UI线程的未处理异常,非UI线程的未处理异常仍然由AppDomain的UnhandledException捕获。当使用了Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException);模式时,UI线程的未处理异常将会被捕获并处理(“吞噬”),在Application.ThreadException事件中可以查阅,这时AppDomain的UnhandledException将不会执行。如果使用的是Application.SetUnhandledExceptionMode(UnhandledExceptionMode.ThrowException);
模式,那么AppDomain的UnhandledException将会捕获所有线程(包括UI线程)的未处理异常信息。
七,宿主如何使用AppDomain
其实这一节才是精华!这一节解释了常见的不同的应用程序是如何寄宿CLR,以及如何管理AppDomain的。
1,可执行应用程序。
这一类程序包括了控制台UI应用程序,Windows窗体应用程序,WPF应用程序,NT Service应用程序,它们都有一个托管的EXE文件。Windows用一个托管EXE加载一个进程时,会加载垫片。垫片会检查包含在EXE文件中的CLR头信息。头信息指明应用程序使用的CLR版本。垫片根据这个信息将对应版本的CLR加载到进程中,CLR加载好后,它会检查CLR的头,判断应用程序的入口方法是哪个(Main)。CLR调用这个方法。这时应用程序会真正的运行起来。
代码运行时,它会访问其他的类型。引用另一个程序集的类型时,CLR会定位所需要的程序集,并把它加载到同一个AppDomain中。应用程序的Main方法结束后,Windows进程终止(销毁默认的AppDomain和其他所有的AppDomain)。
关闭Windows进程,可以调用System.Environment的静态方法Exit。这个方法是终止进程的最得体的方式,因为它首先调用托管堆上的所有对象的Finalize方法,然后释放有CLR持有的所有非托管COM对象。最后调用Win32的ExitProcess函数。
2,Silverlight富Internet应用程序
Silverlight“运行时”技术采用了和.net framework的普通桌面版本有所区别的一个特殊的CLR。安装好Silverlight“运行时”后,每次访问Silverlight技术的一个网站,都会造成Silverlight CLR(CoreClr.dll)加载到浏览器中。网页上的每个Silverlight控件都在它自己的AppDomain中运行。用户关闭标签或切换到另一个网页时,不再使用任何Silverlight控件的AppDomain都会卸载。AppDomain中的Silverlight代码在一个安全性受到限制的沙箱中运行,不会对用户和机器造成损害。
3,Asp.net Web窗体和XML Web服务应用程序
Asp.net作为一个ISAPI DLL(ASPNET_ISAPI.DLL)实现。客户端首次请求一个由Asp.net ISAPI DLL处理的URL时,Asp.net会加载CLR。客户端请求一个Web应用程序时,Asp.net判断是不是第一次请求。如果是,Asp.net会告诉CLR为该WEB应用程序创建一个新的AppDomain;每个Web应用程序都是按照他的虚拟目录来标识的。然后Asp.net指示CLR将包含了“应用程序所公开的类型”的程序集加载到新的AppDomain中,创建该类型的一个实例,并调用其中的方法相应客户端的请求。如果代码引用了更多的类型,CLR会将所需要的程序集加载到Web应用程序的AppDomain中。
未来客户端请求一个已经运行的Web应用程序时,它不会创建新的AppDomain,它会用现有的AppDomain,创建Web应用程序的一个新实例,并调用方法。这些方法已经JIT编译成本地代码,所以客户端的请求处理性能将会比较出色。如果客户端请求的是不同的Web应用程序,Asp.net会告诉CLR创建一个新的AppDomain。每个WEB应用程序需要用到的程序集都会会加载到单独的AppDomain中,这个AppDomain的唯一目的就是将Web应用程序的代码和其他Web应用程序的代码隔离。
Asp.net的另一个出色的功能就是可以在不关闭Web服务器的前提下动态更改网站的代码。网站的文件在硬盘上发生改动时,Asp.net能监测到这种情况,并卸载含有旧版本文件的AppDomain(在当前运行的最后一个请求完成之后),并创建一个新的AppDomain,向其中加载新的版本文件。为确保这个过程顺利进行,Asp.net使用了AppDomain的一个名为“影像复制”(shadow copying)的功能。“影像复制”的含义,大意就是Asp.net真实加载的并不是我们应用程序目录下的dll文件,而是把这些文件拷贝了一份到一个临时目录,在这里加载程序集。当应用程序目录下的文件发生变化时,Asp.net将这些变化了的文件覆盖临时目录里对应的文件,然后卸载旧的AppDomain,创建一个新的AppDomain。
4,Microsoft SQL Server
Microsoft SQL Server是一个非托管的应用程序,因为他的大部分代码都是用C++写的。SQL Server允许开发人员通过托管代码编写存储过程。首次请求用托管代码写的存储过程, SQL Server会加载CLR。存储过程在它们安全的AppDomain中运行,这避免了存储过程对数据库服务器的负面影响。
这其实是一项非常寻常的功能!它意味这开发人员可以选择自己喜欢的编程语言来写存储过程。存储过程在自己的代码中可以使用强类型的数据对象。代码会被JIT编译成本地代码执行。开发人员可以利用FCL或任何其他程序集中定义的任何类型。结果是我们的工作变得越来越轻松。
(全文完)