談談.net對象生命週期

 不用程序員操心的堆 — 託管堆        
c++

  程序在計算機上跑着,就不免會佔用內存資源來存儲在程序運行過程當中的數據,咱們按照內存資源的存取方式將內存劃分爲堆內存棧內存程序員

    棧內存,一般使用的場景是:對存取速度要求較高且數據量不大sql

    典型的棧內存使用的例子就是函數棧,每個函數被調用時都會被分配一塊內存,這塊內存被稱爲棧內存,以先進後出的方式存取數據,在函數執行過程當中不斷往函數棧中壓入(PUSH)數據(值類型數據:int、float、對象的引用...),函數執行完後又將函數棧中的數據逐個彈出(POP),因爲是以操做棧的形式來存取,因此訪問速度快。數據庫

  堆內存,從字面意思上理解就好像是倉庫裏面能夠存一堆破爛,你如果須要存點什麼東西就儘管往裏面一扔,倉庫裏有的是空間。事實確實也是如此,堆內存中能夠存放大規格的數據(好比對象資源),這些數據是不適合存放在棧中的,由於棧空間的容量有限,這就是堆內存相對於棧內存的好處:容量大。可是它的缺點也是顯而易見的,那就是存取堆內存的數據相較於存取棧內存是很是慢的,試想一下,讓你在倉庫裏的一堆破爛裏去找你想要的東西是什麼感受。c#

  (棧內存比堆內存詳細參考:https://blog.csdn.net/boyxiaolong/article/details/8543676函數

  

 

  從內存分配方式上看,堆內存不一樣於棧內存,函數棧是在每個函數被執行的時候被自動分配而且函數執行完成後自動回收,而若是你想使用堆內存,就得本身動手豐衣足食。優化

    因此你會看到c語言程序員會這樣去使用堆內存:this

int *p = (int*)malloc(sizeof(int)); //在堆內存中申請一塊字節數爲int字節數的堆內存,並返回指向該內存區域的指針
*p = 10; 
free(p); //釋放堆內存資源

   你還會看見c++程序員這樣寫:spa

Car* bmw = new Car(); //建立一個Car類對象,在堆內存中存放對象數據,並返回指向對象資源的指針
delete bmw; //釋放堆內存資源

  固然,沒有接觸過c/c++的小夥伴也不用驚慌,上面只不過是想讓你知道在c/c++語言中,程序員要是想使用堆內存,那就必須顯式地編寫分配和釋放堆內存資源的代碼。操作系統

    有人問使用完堆內存資源後沒有手動釋放它會有什麼後果嗎

    答案是因爲堆內存資源使用者未及時釋放內存會致使內存沒法再次使用,從而形成內存資源的泄漏(浪費)。

 

    就在這個時候,c#程序員笑了,只見他的手指很是輕盈優雅地在屏幕上敲出了下面這行代碼:

  Car bmw = new Car();

  一旁圍觀的c程序員和c++程序員驚呆了,他們不知道本身在敲代碼的時候有沒有像這樣輕鬆過。c++程序員用手撫摸着他那鋥光瓦亮的額頭,忽然眼睛裏閃着光,喊道:「你尚未釋放堆內存的資源呢,你這樣是很危險的,會內存泄漏的,快,把釋放堆內存的代碼寫上!」

  c#程序員彷佛並不爲所動,舒舒服服地靠在椅子上,用餘光瞟了c++程序員一眼,說:「不用慌,不用慌,這個對象在託管堆上放的好好的呢,不用我操心」,因而,c#程序員便娓娓道來(呼呼大睡)...

 

  在.NET的世界,使用new關鍵字建立一個對象,首先對象資源被分配在託管堆中,而後new會返回一個指向堆上對象的引用,而不是真正的對象自己。若是在方法做用域中將引用變量聲明爲本地變量,這個引用變量保存在棧內,以供應用程序之後使用。

 

   託管堆,顧名思義,就是託給別人管的堆,那麼是誰在管理着這個堆上的對象資源呢

  答案是CLR(Common Lanauage Runtime),對象的實例化結束之後,GC(垃圾回收器)將會在對象再也不須要時將其銷燬。

  也就是說,經過容許垃圾收集器負責銷燬對象,內存管理的麻煩就都交給CLR了,萬事大吉。

 

 

 

 

 

  看似問題好像都已水落石出,無非就是將堆內存資源回收交給了CLR去承擔。難道你就不想知道的更多一點?好比接着而來的問題:

  1垃圾回收器如何判斷一個對象何時再也不須要?

  二、垃圾回收器又在何時會執行垃圾清理的操做?

  別急,帶着問題慢慢往下看。

CIL的new指令 — 垃圾回收的觸發者

  c#中的new關鍵字最終會被編譯器翻譯成CIL的newobj指令,讓咱們仔細查看一下CIL newobj指令的做用。

  首先,須要明白託管堆不只僅是一個可由CLR訪問的隨機內存塊。.NET垃圾回收器是堆的「清潔工」,出於優化的目的它會壓縮空閒的內存塊(當須要時)。爲了輔助壓縮,託管堆會維護一個指針(一般被叫作下一個對象指針或者是新對象指針),這個指針用來標識下一個對象在堆中分配的地址。

  此外,newobj指令通知CLR來執行下列的核心任務:

  (1)計算要分配的對象所需的所有內存(包括這個類型的數據成員和類型的基類所需的內存)。

  (2)檢查託管堆來確保有足夠的空間來放置所申請的對象。若是有足夠的空間,會調用這個類型的構造函數,構造函數會返回一個指向內存中這個新對象的引用,這個新對象的地址恰好就是下一個對象指針上一次所指向的位置。

  (3)最後,在把引用返回給調用者以前,讓下一個對象指針指向託管堆中下一個可用的位置。

  下面的圖解釋了在託管堆上分配對象的細節。

       

  在c#中分配對象是一個很頻繁的操做,照這樣下去託管堆上的空間早晚會被揮霍完,因此,重點來了,若是CLR 發現託管堆沒有足夠空間分配請求的類型時,它會執行一次垃圾回收來釋放內存

  當執行垃圾回收時,垃圾收集器臨時掛起當前進程中的全部的活動線程來保證在回收過程當中應用程序不會訪問到堆。(一個線程是一個正在執行的程序中的執行路徑)。一旦垃圾回收完成,掛起的線程又能夠繼續執行了。還好,.NET 垃圾回收器是高度優化過的,因此用戶不多能察覺到應用程序中的短暫中斷。

  經過對CIL的new指令做用的解讀,咱們知道了:若是託管堆沒有足夠的空間分配一個請求的對象,則會執行一次垃圾回收

 

     (講到這裏c#程序員停了下來,喝了口保溫杯裏的枸杞紅棗大補茶🍵,清了清嗓子,繼續開始解惑...)

       

應用程序根的做用 區分不可到達的對象

  如今讓咱們來討論一下垃圾回收器怎樣肯定何時「再也不須要」一個對象。爲了理解細節,你須要知道應用程序根的概念。

  簡單來講,一個根是一個引用,這個引用指向堆上面的一個對象的。嚴格來講,一個根能夠有如下幾種狀況:

    (1) 指向全局對象的引用(儘管C#不支持,但CIL代碼容許分配全局對象)

  (2) 指向任何靜態對象

  (3) 指向一個應用程序代碼中的局部對象

  (4) 指向傳入到一個函數中的對象參數

  (5) 指向等待被終結(finalized)的對象

  (6) 任何一個指向對象的CPU寄存器

  在一次垃圾回收的過程當中,運行環境會檢查託管堆上面的對象是否仍然是從應用程序根可到達的。爲了檢查可達,CLR會創建一個表明堆上每一個可達對象的圖。對象圖用來記錄全部可達的對象。同時,注意垃圾回收器毫不會在圖上標記一個對象兩次,所以避免了煩人的循環引用。

  假設託管堆上有名字爲A,B,C,D,E,F和G的對象集合。在一次垃圾回收過程當中,會檢查這些對象(同時包括這些對象可能包含的內部對象引用)是不是根可達的。一旦圖被創建起來,不可達的對象(在此是對象C和F)被標記爲垃圾。

  下圖是上述場景的一個可能的對象圖(你能夠把箭頭讀做依賴或者須要,例如"E依賴於G,間接依賴於B,「A不依賴任何對象」等)。

(建立的對象圖是用來決定哪些對象是應用程序根可達的。)

  一旦一個對象已經被標記爲終結(此例子中是C和F--在圖中沒有他倆),它在內存中就被清理掉了。在此時,堆上的剩餘內存空間被壓縮,這會致使CLR修改活動的應用程序根集合(和對應的指針)來指向正確的內存位置(這個操做是自動透明的)。最後,調整下一個對象指針來指向下一個可用的內存位置。

  下圖闡明瞭清除和壓縮堆的過程。

  到這裏,經過對應用程序根的做用的理解,咱們知道了如何知道一個對象是「再也不須要」的。通俗點來講就是,這個對象在應用程序中已經無需被訪問了,成爲了一座「孤島」,天然也就再也不須要它了。

 

  爲了讓c++程序員能更加理解. net垃圾回收的奧妙,c#程序員繼續口若懸河…

 

理解對象的代 — 垃圾回收過程的優化

  在嘗試找到不可達的對象時,CLR並非檢查託管堆上的每一個對象。很明顯,這樣作會消耗大量時間,尤爲在大型(例如現實中)程序中。

  爲了幫助優化這個過程,堆上的每一個對象被分配到一個特殊的"代」。代這個概念背後的想法很簡單:對象在堆上存活的時間越長,接下來它繼續存在的可能性也就越大,即較舊的對象生存期長,較新的對象生存期短。例如,實現Main()的對象一直在內存中,直到程序結束。相反,最近才被放到堆中的對象(例如在一個函數範圍裏分配的對象)極可能很快就不可達。

  在堆上的每一個對象屬於如下的某一個代:

    Generation 0: 標識一個最近分配的尚未被標記爲回收的對象

  Generation 1: 標識一個經歷了一次垃圾回收而存活下來的對象(例如,他被標記爲回收,但因爲堆空間夠用而沒有被清除掉)

  Generation 2:標識一個經歷了不止一輪垃圾回收而存活下來的對象。

 

  垃圾回收器首先會檢查generation 0的全部對象。若是標記並清理這些對象(譯者注:由於新對象的生存期每每較短,而且指望在執行回收時,應用程序再也不使用第 0 級託管堆中的許多對象)後產生了足夠使用的內存空間,任何存活下來的對象就被提高到Generation 1。爲了理解一個對象的代如何影響回收的過程,能夠查看下圖。下圖解釋了generation 0中一次垃圾回收後,存活的對象被提高的過程。  

(generation 0 中的存活對象被提高到generation 1)

  若是全部的generation 0對象都被檢查了,可是產生的內存空間仍然不夠用,就檢查一遍generation 1中的全部對象的可達性並回收。存活下來的generation 1對象被提高到generation 2。若是垃圾回收器仍然須要額外的內存,generation 2的對象就經歷檢查並被回收。此時,若是一個generation 2的對象存活下來,它仍然是一個generation 2的對象。

  其實經過對象的代的設計是想達到這麼一個效果:新對象(好比局部變量)會被很快回收,而老一些的對象(如一個應用程序對象)不會被常常騷擾

  說到底,對象代的設計就是爲了優化垃圾回收的過程。   

 

  「我還有最後一個問題」,c++程序員按耐不住內心一直的疑惑,說到:「你說了這麼多都是再講託管資源,難道.net中就沒有非託管資源嗎?. net又是怎麼對非託管資源進行資源釋放的呢?」。

  "這個問題問的好!",c#程序員大笑,因而接着又開始解惑(吹B)…

 

構建可終結對象 — 非託管資源處理第一式

  以一名c#開發者的直覺告訴你,大多數的c#類都不須要顯式的清理邏輯。緣由很簡單:若是類型使用了其餘託管對象,一切都最終會被垃圾回收。

  問:那在何時須要顯式地清理呢

  答案是:在你使用非託管資源時(例如原始的操做系統文件句柄、原始的非託管數據鏈接或其餘非託管資源),纔可能須要設計一個在用完後清理自身垃圾的類

  好比說下面這個類:

   //數據庫上下文類
   public class SqlDbContext
   {
       //...(其餘被引用的對象實例)

       //類中包含的非託管資源(須要調用 Dispose()函數進行資源的釋放)
       SqlConnection sqlConnection = new SqlConnection("...");  
            
   }    

  如今問題來了,咱們要在適當的時機調用數據庫鏈接類對象釋放資源的方法(SqlConnection類對象使用完後須要調用Dispose()方法釋放資源)。這個適當的時機固然就是對象在被CLR進行垃圾回收的過程當中,因此問題又來到了,有沒有一個方法是在這個時機被調用,並且是能夠被擴展的呢

  是的,咱們能夠利用. NET的基類System.Object中定義的名爲Finalize()的虛方法,也叫做終結器方法,它是這樣的:

  

 

 

   看到這固然會很奇怪,不是說有Finalize()方法,在哪,逗我?莫驚訝,其實這裏的 ~Object() 就是Finalize(),只是一個語法糖罷了。

 

  Finalize()的調用將(最終)發生在一次"天然的"垃圾回收或用程序經過GC.Collect()強制回收的過程當中,因此這樣看來,終結器方法就是讓類對象釋放內部非託管資源的地方。nice,如今咱們能夠像這樣來編寫清理非託管資源的代碼:

        //數據庫上下文類
        public class SqlDbContext
        {
            //...(其餘被引用的對象實例)

            //類中包含的非託管資源(須要調用 Dispose()函數進行資源的釋放)
            SqlConnection sqlConnection = new SqlConnection("...");  

            ~SqlDbContext()
            {
                //這裏清除非託管資源
                this.sqlConnection.Dispose();
            }
            
        }    

  這樣被構建的對象被叫作可終結對象

  有關於終結過程的細節,在《C#與.NET4高級程序設計(第5版)》書中是這樣描述的:

  

 

 

   從以上的內容咱們得知:經過Finalize()來清除非託管資源的時機只能是在.NET對象被垃圾回收的過程當中,並且終結過程是一個消耗不小的動做

  問題又來了不少非託管資源都很是寶貴(如數據庫和文件句柄),因此這些資源應該在使用完後儘快地被清除,而不能依靠垃圾回收的發生,那麼這些資源應該以怎樣的形式被顯示地釋放呢?

 

     

構建可處置對象 — 非託管資源處理第二式

      除了重寫 Finalize() 以外,類還能夠實現 IDisposable 接口,它定義了一個名爲 Dispose() 的方法:

    public interface IDisposable
    {
        void Dispose();
    }

 

  它的使用方法就是:在類的Dispose()方法中編寫非託管資源的釋放的代碼,程序員能夠在這個對象再也不須要的時候手動調用對象的Dispose()方法來達到及時釋放非託管資源的目的。

    因而你能夠像這樣來編寫類:

        //數據庫上下文類
        public class SqlDbContext:IDisposable
        {
            //...(其餘被引用的對象實例)

            //類中包含的非託管資源(須要調用 Dispose()函數進行資源的釋放)
            SqlConnection sqlConnection = new SqlConnection("...");  

            public void Dispose()
            {
                //這裏清除非託管資源
                this.sqlConnection.Dispose();
            }
        }        

    採用這種方式來釋放非託管資源的類被稱做爲可處置對象

    在這裏還要補充一點,C#提供了一個語法糖來簡化調用Dispose()操做,以下:

   SqlDbContext context = new SqlDbContext();

   try
   {
        //在此做用域內使用SqlDbContext類對象context
   }
   finally
   {
        //確保使用完後調用Dispose()方法
        context.Dispose();
   }    

   上面這段代碼等同於下面這段代碼:

  using (SqlDbContext context = new SqlDbContext())
  {
        //在此做用域內使用SqlDbContext類對象context
  }

   

  c++程序員說:「你這還不是要本身手動調用,若是我忘記調用 Dispose() 那豈不是一切都玩完!」

     c#程序員冷笑一聲,「非也,非也,我來傳授你最後一招吧!」

非託管資源最強模式 — 雙劍合璧

  人非聖賢,孰能無過。程序員也會有失手的時候,好比,忘記調用 Dispose() 方法...

  這個時候就必須設計一個萬無一失的方法,達到一個目的:就是無論有沒有手動調用Dispose(),非託管資源最終都應該被妥妥地釋放掉。爲了解決這個問題,咱們能夠以下去定義一個可處置對象類:

        //數據庫上下文類
        public class SqlDbContext:IDisposable
        {
            //...(其餘被引用的對象實例)

            //類中包含的非託管資源(須要調用 Dispose()函數進行資源的釋放)
            SqlConnection sqlConnection = new SqlConnection("...");  

            ~SqlDbContext()
            {
                //這裏清除非託管資源
                this.sqlConnection.Dispose();
            }

            public void Dispose()
            {
                //這裏清除非託管資源
                this.sqlConnection.Dispose();

                //跳過終結過程
                GC.SuppressFinalize(this);
            }    

 

  能夠看到,這個類中即有終結方法的重寫也有Dispose()方法,這樣就能保證:程序員若忘記調用Dispose()方法釋放非託管資源,那麼對象就會在垃圾回收的過程當中調用終結方法來釋放非託管資源;若程序員調用了Dispose()方法,那麼 GC.SuppressFinalize(this) 會保證在垃圾回收過程當中再也不會調用對象的終結方法,避免沒必要要的資源開銷。可謂「雙劍合璧」,保萬無一失。

 

  

  話音剛落,c++程序員「噗通」一聲跪倒在c#程序員面前,雙手死死拉住c#程序員的褲子,"師父,收我爲徒吧!我也要學c#…",c#程序員不想本身的褲子被扯破,因而答應了他。掏出一本上古神書…

  

 

 

  哦,不,拿錯了。。。應該是這本。。。

 

  

 

  全劇終…

相關文章
相關標籤/搜索