《C#與.NET3.5進階程式設計(第4版)》筆記8

來源:互聯網
上載者:User

第八章 對象的生命週期

本章主要介紹了CLR怎樣通過記憶體回收來管理已指派的對象。.NET對象被分配到一塊叫做託管堆的記憶體地區上,在那裡它們會在將來的某個時刻被記憶體回收行程(以下簡稱GC)自動銷毀。之後瞭解了System.GC類型通過編程使用記憶體回收行程。接著分析了System.Object.Finalize()虛方法和IDisposable介面建立及時釋放內部非託管資源的類型。

8.1 類、對象和引用

類是一個藍圖,描述了這個類型的執行個體(對象)在記憶體中是什麼樣子,類是定義在一個代碼檔案中的。

對象是在定義一個類後,就可以使用C#的new關鍵字分配任意數量的對象,在託管堆中為其分配記憶體。但是,new關鍵字返回的是指向堆上對象的引用,而不是真正對象本身。這個引用變數儲存在棧中,以供應用程式使用。因此,從整體上看,一個對象由在棧中的引用和在託管堆中的對象實體構成。

8.2 記憶體管理法則

在建立c#應用程式時,無需對託管堆進行直接操作,它將自行管理。.NET進行記憶體管理的法則主要有四條,下面章節中分別介紹。

(1) 法則1:使用new關鍵字將一個對象分配到託管堆中,然後不用再管。

(2) 如果託管堆沒有足夠記憶體來分配所請求的對象,就會進行記憶體回收。

(3) 重寫Finalize唯一的原因是,c#類通過pinvoke或者複雜的COM互通性任務使用了非託管資源。

(4) 如果對象支援IDisposable,總是要對任何直接建立的對象調用Dispose(),應該認為,如果類設計者選擇支援Dispose方法,這個類型就需要執行清理工作。

8.3對象聲明周期基礎(法則1)

根據法則1,執行個體化結束之後,GC將會在對象不再需要時將其銷毀。如何判斷一個在託管堆中對象不再需要呢?簡單說,就是一個對象從程式碼程式庫中任何部分都不可達時,就會在記憶體回收時將其從堆中刪除。例如,對於一個方法中定義的一個對象,那麼並且這個對象沒有被傳遞到方法定義範圍之外,則當方法調用結束後,這個對象的引用就不在可達,相關聯這個對象就是記憶體回收的候選目標。但是,不能保證他成為候選回收目標後就立刻被回收,可以肯定的是,當CLR進行下一次記憶體回收時,將被完全的銷毀。

疑問:“可以肯定的是,當CLR進行下一次記憶體回收時,將被完全的銷毀。”,誰知道是不是僅僅是被標記為下一代還是真的就回收了呢??

自動記憶體回收大大簡化應用程式開發,避免了記憶體泄露(未及時釋放記憶體而導致記憶體無法再試試用,導致浪費)的問題。

New關鍵字在CLR中對應一條newobj指令,為了輔助託管堆進行記憶體回收,託管堆儲存了一個指標(稱為下一個對象指標或新對象指標),它精確指示下一個對象將被分配的位置。

在newobj發出指令後,CLR將執行下列核心任務:

1)計算指派至所需記憶體總數(包括成員變數和類型的基類的必須記憶體)。

2)檢查託管堆,確保有足夠空間來訪只要分配的對象,如果有,則調用類型的建構函式,在託管堆中為其分配空間,並將當前的下一個對象指標作為這個對象的引用返回給調用者,然後移動下一個對象的指標,指向託管堆下一個可用的位置;若空間不夠,則根據法則2,它會實行一次記憶體回收來嘗試釋放記憶體,釋放後有了新的下一個對象的指標,然後按照上面有足夠空間的邏輯進行分配。

8.4記憶體回收過程(法則2)

當確實發生回收時,GC掛起當前進程中所有活動的線程,以保證應用程式在回收過程中不會訪問堆。一旦回收周期完成,就允許掛起線程繼續他們的工作。由於GC進行了精心最佳化,因此使用者很少察覺到程式中斷。

已經說了,記憶體回收將會在將來的某個時間發生,而這個過程往往是不確定的(除非強制記憶體回收),因此,一些程式員按照之前c++的方法講一個不再使用的對象設定為null,認為這就可以被立刻回收而釋放記憶體了,這種想法是不對的,在c#中,將引用賦值為null並不意味強制GC立刻啟動並把對象從堆中移除,它僅僅是顯示取消了引用和之前引用所指向對象之間的串連(我認為僅僅是你無法再訪問這個對象了,因為引用為空白了,但是這個對象還是真實存在的,另外,棧中的引用由於設定為null,則立刻彈出棧,成為可用性區域域!)。因此,設定null的意義並不大,但是也沒什麼害處。

在記憶體回收啟動時時,依賴於兩個重要概念:應用程式根和對象的代。

應用程式根:前面說過一個對象不可達時候就是可以作為記憶體回收的備選了,但是如何確定不可達呢,這就需要應用程式跟,根就是一個儲存位置,儲存著對堆上一個對象的引用。

記憶體回收時,首先運行庫將檢查託管堆中的對象(和他們可能包含的任何內部對象引用),判斷應用程式是否仍然可達到他們,也就是是否有根或者說是否有活動根。這個根的產生是根據對象圖產生的。對象圖包含堆上可達的每一個對象,而且不會讓同一個對象在對象圖中出現一次以上。一旦產生了對象圖,不可達的對象就被標記為垃圾。標記為終結後,他們就會從記憶體中清除(不一定都清除哦,空間釋放夠了就可能暫時不清除,這在下面對象的代中可以解釋),剩餘空間被壓縮(稱為重新置放),繼而引起CLR修改活動應用程式根的集合,指向正確的記憶體位置,最後,下一個對象的指標被重新調整以指向下一個可用位置。

實際上,GC使用兩個不同的堆,一個專門用於儲存非常大的對象,這個堆得記憶體回收中較少顧及,因為重新置放的開銷很大。

對象的代:上面說了,在記憶體回收時CLR會檢查對象是否不可達對象,但是並不是檢查託管堆中每一個對象,顯然,全部檢查將消耗大量時間。為了最佳化這個過程,堆上的每個對象被指定為屬於某“代”,其設計思路就是:對象在堆中存在時間越長,就更可能應該保留。每個堆的對象屬於下列某代:

第0代:從沒有被標記為回收的新分配的對象;

第1代:在上一次記憶體回收中沒有被回收的對象(也就是,他被標記為回收了,但是因為已經擷取了足夠的空間,而暫時沒有被刪除);

第2代:在上一次以上的記憶體回收後仍然沒有被回收的對象。

在檢查對象時,首先檢查第0代,如果標記和清除這些對象後得到所需數量空閑記憶體,任何沒有被回收的對象被提升為第1代。如果算上第0代上所有對象後仍然需要更多記憶體,就會檢查第1代對象的“可達性”,並相應的進行回收。沒有被回收的第1代對象隨後被提升到第2代。如果仍然需要更多記憶體,就會檢查第2代對象可達性,這時,如果一個第2代對象在記憶體回收後仍然存在,它仍然是第2代對象,因為這是對象代的上限。

疑問:“如果算上第0代上所有對象後仍然需要更多記憶體”,幹嘛要算上所有對象記憶體而不是剩餘的第0代空閑空間呢?

疑問:若第2代回收後還是不夠呢?

這裡的要點是,通過給堆上對象賦一個表示代的值(當然是系統來賦值,他會根據這個對象的類型和範圍等情況進行分配),儘快的刪除一些較新的對象(比如本地變數),而不會經常打擾一些舊的對象(例如應用程式物件)。也就是一個對象有可能剛剛分配就被放在第2代,這是可以的,不是必須從0代開始依次分配。

.NET平台提供了一個名為System.GC的類類型,他通過編程使用一個靜態成員集合與GC進行互動,方便查看託管堆的情況和記憶體回收資訊。例如統計某一代進行了多少次記憶體回收、一個對象當前的代、顧及分配給的託管堆的記憶體數量、系統支援的代的數量等。其中一個最為重要的是GC.Collect方法,在一些非常罕見的情況下,通過編程用此方法強制進行記憶體回收可能會有好處,就是:

應用程式將要進入一段代碼,後者不希望被可能得記憶體回收中斷;

應用程式剛剛分配非常多的對象,想儘可能多的刪除已獲得的記憶體。

此時需要顯式的觸發一次記憶體回收,在手動強制回收時,總是調用GC.WaitForPendingFinalizers(),這樣,就可以稍等片刻,已確定在程式繼續執行之前,所有可終結的對象必須執行所有必要的清理工作。在底層,WaitForPendingFinalizers會在回收過程中掛起調用的線程,這是件好事,保證代碼不調用當前正在被銷毀的對象的方法。

GC.Collect();/*也可以加入第一個數值參數,指定是對某一個代進行強制回收,也可加入第二個參數,用於調整運行庫如何強制進行記憶體回收*/

GC.WaitForPendingFinalizers();

疑問:不調用GC.WaitForPendingFinalizers()會怎麼樣呢?

需要注意的是,即使只做了一次記憶體回收的顯示請求,CLR在幕後也執行了多次記憶體回收。

8.5構建可終結的對象(法則3)

在第六章中,知道了Object類有一個Finalize方法,這個預設實現是什麼都不做,這個方法是保護層級,因此不可能通過點運算子從類執行個體(對象)直接調用一個對象的Finalize方法,也就是不能手動的直接調用。相反,只能自動發生於一次自然的記憶體回收或者通過GC.Collect強制回收的過程中,或者當成在應用程式的應用程式定義域從記憶體卸載時的過程中,在從記憶體刪除這個對象時,GC會自動調用這個方法(如果支援這個方法的話)。也就是CLR自動調用在它的生命週期中建立的每一個可終結對象的終結器(解構函式)。

另外,對實值型別的Finalize重寫是不合法的,因為實值型別本身就不在託管堆,無法被記憶體回收。

大多數C#類不需要顯式清理邏輯,也不需要自訂終結器(解構函式),因為如果使用的是託管對象,一切最終會被回收。只是在使用非託管資源時,才可能需要設計一個用完後清理自身的類。這就是第三條原則。

法則3清理非託管資源,需要重寫System.Object.Finalize()方法,以便在記憶體回收時,被自動調用。但是,比較奇怪的是,這個重寫不再使用override關鍵字(protected override void finalize),這會導致編譯錯誤。C#提供了解構函式文法來達到同樣效果。解構函式和建構函式類似,必須和類同名,但是不能有參數,不能有傳回型別,只能一個類有一個解構函式,形式如下(注意波折號):
~myresourcewarpper()

{

//清理這裡非託管資源

}

實際上,CLR在IL中繼語言中,還是會將其轉換為Finalize方法,而且加入了必要的錯誤偵測代碼(try/catch)。

一些細節:應該牢記Finalize方法的作用是保證.NET能在記憶體回收時清除非託管資源,如果建立了一個不使用非託管實體的類型,終結是沒有用的。事實上,如果可能,應該避免在設計類時提供Finalize方法(解構函式),因為終結要花費時間。

整個過程是:在託管堆中指派至時,運行庫自動確定該對象是否提供了自訂的Finalize方法,如果是,對象被標記為可終結,同時一個指向這個對象的指標被儲存在名為終結隊列的內部隊列中,終結隊列是由GC維護的表,它指向每個再從堆上刪除之前必須被終結的對象。

當GC確定到了從記憶體釋放一個對象時,他檢查終結隊列上每項,並將對象從堆上複製到另一個稱作終結可達表的託管結構中,此時,下一個記憶體回收時將產生另外一個線程,為每一個在可達表中的對象首先調用Finalize方法,因此,為了真正終結一個對象,至少需要兩次記憶體回收。

總之,儘管對象的終結能夠保證對象可以清除非託管資源,但是本質上仍然是非確定的,而且由於額外的幕後處理,速度變得相當慢。

疑問:“如果建立了一個不使用非託管實體的類型,終結是沒有用的”不太理解?

8.6構建可處置的對象(法則4)

上面說了,為了在記憶體回收時清理非託管資源,可以利用終結器。但是,因為很多非託管資源是很寶貴的,所以應該儘可能快的被清除,而不是依靠記憶體回收的發生。除了重寫Finalize,類還可以實現IDisposable介面。如果提供了這個介面,就是假設當對象的使用者不再使用這個對象時,會在這個引用離開範圍之前手動的調用Dispose方法。這樣,就可以執行任何必要的非託管資源清理,而且不會再有將對象放在終結隊列上導致的效能損失,而且不必等待垃圾GC觸發類的終結邏輯。這就是法則4。

注意,實值型別和參考型別都可以調用Dispose方法,因為二者都可以實現IDisposable介面。

Public class myresourcewarpper:IDisposable

{

Public void Dispose()

{

//清除非託管資源

//拋棄包含的其他可處置對象

}

}

Dispose方法不只是負責釋放一個對象非託管資源,還應該對任何它包含的可處置對象調用Dispose方法。與Finalize不同,Dispose方法與其他託管對象的通訊時安全的,因為GC並不支援IDisposable介面,因此永遠不會自動調用Dispose方法,顯然,Dispose方法會在GC之前的某個手工調用時候發生,不會擔心要處置的資源已經被回收而出錯。但是這也有個弊端,就是實現了Dispose方法來釋放非託管資源,但是如果使用時忘記手動調用,那麼非託管資源可能永遠存於記憶體當中。

判斷一個類型是否支援IDisposable介面,可以查看SDK,也可以通過is或as關鍵字判斷是否支援此介面。

另外,和Finalize類似,積累庫中許多都實現了IDisposable的類,還提供了Dispose方法的別名,例如fs.colse()等價於fs.dispose(),但是,調用後者總是正確的。

為了保證手動調用Dispose方法而把每個可處置的類型顯式的封裝在try/catch塊中,顯然很麻煩,因此,C#提供了特殊文法,利用using關鍵字,來自動調用Dispose方法,並在IL中擴充了Dispose方法,加入了try/catch邏輯。

Using(myresourcewarpper rw=new myresourcewarpper())

{

//使用rw對象

}

這個文法結構保證在退出using塊後,正在使用的對象(rw)將自動調用其Dispose方法。同時,也可以在using範圍中聲明相同類型的多個對象(用逗號分開),編譯器會插入代碼來調用每一個對象的Dispose方法。

8.7構建可終結和可處置的類型

上面介紹了兩種釋放非託管資源的方式。一方面可以重寫Finalize方法,在記憶體回收時被自動調用,另一方面,實現Dispose方法,讓使用者手動調用並釋放非託管資源。兩種技術各有特點,也有各自的弊端,將兩種方式放入同一個類,是可行的好方法。

這樣,如果使用者記住調用了Dispose方法,那麼可以通過GC.SuppressFinalize()方法來通知GC跳過終結過程(調用Finalize方法),如果使用者忘記調用Dispose方法,那麼最終對象也會被GC通過調用Finalize方法而釋放。總之,對象的內部非託管資源會用其中一種方式釋放掉。

正式的處置方式微軟給出了一個有些刻板的處置模式(MyResourceWrapper版本),他在健壯性、可維護性和效能三者間取了平衡。它確保了Finalize方法不會嘗試處置任何託管對象,而Dispose方法應該這樣做,並且確保對象使用者可以安全地多次調用Dispose方法而不出錯,還將兩個方法都需要調用的代碼通過輔助私人函數實現代碼的簡練。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.