雁过请留痕...
代码改变世界

《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或任何其他程序集中定义的任何类型。结果是我们的工作变得越来越轻松。

 

(全文完)