《CLR via C#》筆記——AppDomain(2)


四,卸載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或任何其他程序集中定義的任何類型。結果是我們的工作變得越來越輕松。

 

(全文完)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM