談談 .net 對象生命週期

來源: cnblogs.com/MaMaNongNong/p/11945161.html

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

程序在計算機上跑著,就難免會佔用內存資源來存儲在程序運行過程中的數據,我們按照內存資源的存取方式將內存劃分為堆內存棧內存


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


典型的棧內存使用的例子就是函數棧,每一個函數被調用時都會被分配一塊內存,這塊內存被稱為棧內存,以先進後出的方式存取數據。


在函數執行過程中不斷往函數棧中壓入(PUSH)數據(值類型數據:int、float、對象的引用...),函數執行完後又將函數棧中的數據逐個彈出(POP),由於是以操作棧的形式來存取,所以訪問速度快。

談談 .net 對象生命週期

堆內存,從字面意思上理解就好像是倉庫裡面可以存一堆破爛,你若是需要存點什麼東西就儘管往裡面一扔,倉庫裡有的是空間。


事實確實也是如此,堆內存中可以存放大規格的數據(比如對象資源),這些數據是不適合存放在棧中的,因為棧空間的容量有限,這就是堆內存相對於棧內存的好處:容量大。


但是它的缺點也是顯而易見的,那就是存取堆內存的數據相較於存取棧內存是非常慢的,試想一下,讓你在倉庫裡的一堆破爛裡去找你想要的東西是什麼感覺。

棧內存比堆內存詳細參考:

blog.csdn.net/boyxiaolong/article/details/8543676


談談 .net 對象生命週期

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


所以你會看到c語言程序員會這樣去使用堆內存:

談談 .net 對象生命週期

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


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


答案是:由於堆內存資源使用者未及時釋放內存會導致內存無法再次使用,從而造成內存資源的洩漏(浪費)

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

<code>Carbmw=newCar();/<code>

一旁圍觀的c程序員和c++程序員驚呆了,c++程序員突然眼睛裡閃著光,喊道:“你還沒有釋放堆內存的資源呢,你這樣是很危險的,會內存洩漏的,快,把釋放堆內存的代碼寫上!”


c#程序員似乎並不為所動,舒舒服服地靠在椅子上,用餘光瞟了c++程序員一眼,說:“不用慌,不用慌,這個對象在託管堆上放的好好的呢,不用我操心”,於是,c#程序員便娓娓道來(呼呼大睡)...

在.NET的世界,使用new關鍵字創建一個對象,首先對象資源被分配在託管堆中,然後new會返回一個指向堆上對象的引用,而不是真正的對象本身。


如果在方法作用域中將引用變量聲明為本地變量,這個引用變量保存在棧內,以供應用程序以後使用。

 

談談 .net 對象生命週期

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


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


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

 

談談 .net 對象生命週期

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

1、垃圾回收器如何判斷一個對象什麼時候不再需要?

2、垃圾回收器又在什麼時候會執行垃圾清理的操作?


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


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

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

談談 .net 對象生命週期

首先,需要明白託管堆不僅僅是一個可由CLR訪問的隨機內存塊。


.NET垃圾回收器是堆的“清潔工”,出於優化的目的它會壓縮空閒的內存塊(當需要時)。


為了輔助壓縮,託管堆會維護一個指針(通常被叫做下一個對象指針或者是新對象指針),這個指針用來標識下一個對象在堆中分配的地址。


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

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


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


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


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

談談 .net 對象生命週期

在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不依賴任何對象”等)。

談談 .net 對象生命週期

創建的對象圖是用來決定哪些對象是應用程序根可達的


一旦一個對象已經被標記為終結(此例子中是C和F--在圖中沒有他倆),它在內存中就被清理掉了。


在此時,堆上的剩餘內存空間被壓縮,這會導致CLR修改活動的應用程序根集合(和對應的指針)來指向正確的內存位置(這個操作是自動透明的)。


最後,調整下一個對象指針來指向下一個可用的內存位置。


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

談談 .net 對象生命週期

到這裡,通過對應用程序根的作用的理解,我們知道了如何知道一個對象是“不再需要”的。通俗點來說就是,這個對象在應用程序中已經無需被訪問了,成為了一座“孤島”,自然也就不再需要它了

(為了讓c++程序員能更加理解. net垃圾回收的奧妙,c#程序員繼續滔滔不絕…)

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

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


為了幫助優化這個過程,堆上的每個對象被分配到一個特殊的"代”。


代這個概念背後的想法很簡單:對象在堆上存活的時間越長,接下來它繼續存在的可能性也就越大,即較舊的對象生存期長,較新的對象生存期短


例如,實現Main()的對象一直在內存中,直到程序結束。相反,最近才被放到堆中的對象(例如在一個函數範圍裡分配的對象)很可能很快就不可達。


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

  • Generation 0:標識一個最近分配的還沒有被標記為回收的對象
  • Generation 1: 標識一個經歷了一次垃圾回收而存活下來的對象(例如,他被標記為回收,但由於堆空間夠用而沒有被清除掉)
  • Generation 2:標識一個經歷了不止一輪垃圾回收而存活下來的對象。

垃圾回收器首先會檢查generation 0的所有對象。如果標記並清理這些對象(譯者注:因為新對象的生存期往往較短,並且期望在執行回收時,應用程序不再使用第 0 級託管堆中的許多對象)後產生了足夠使用的內存空間,任何存活下來的對象就被提升到Generation 1。


為了理解一個對象的代如何影響回收的過程,可以查看下圖。下圖解釋了generation 0中一次垃圾回收後,存活的對象被提升的過程。

談談 .net 對象生命週期

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#類都不需要顯式的清理邏輯。原因很簡單:如果類型使用了其他託管對象,一切都最終會被垃圾回收。


問:那在什麼時候需要顯式地清理呢


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


比如說下面這個類:

談談 .net 對象生命週期

現在問題來了,我們要在適當的時機調用數據庫連接類對象釋放資源的方法(SqlConnection類對象使用完後需要調用Dispose()方法釋放資源)。這個適當的時機當然就是對象在被CLR進行垃圾回收的過程中。


所以問題又來到了,有沒有一個方法是在這個時機被調用,而且是可以被擴展的呢


是的,我們可以利用. NET的基類System.Object中定義的名為Finalize()的虛方法,也叫作終結器方法,它是這樣的:

 

談談 .net 對象生命週期

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

Finalize()的調用將(最終)發生在一次"自然的"垃圾回收或用程序通過GC.Collect()強制回收的過程中,所以這樣看來,終結器方法就是讓類對象釋放內部非託管資源的地方。


nice, 現在我們可以像這樣來編寫清理非託管資源的代碼:

談談 .net 對象生命週期

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


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

談談 .net 對象生命週期

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

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

談談 .net 對象生命週期

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

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

<code>publicinterfaceIDisposable{
voidDispose();
}/<code>

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


於是你可以像這樣來編寫類:


談談 .net 對象生命週期

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


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

談談 .net 對象生命週期

c++程序員說:“你這還不是要自己手動調用,如果我忘記調用 Dispose() 那豈不是一切都玩完!”


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


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

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


這個時候就必須設計一個萬無一失的方法,達到一個目的:就是不管有沒有手動調用Dispose(),非託管資源最終都應該被妥妥地釋放掉。


為了解決這個問題,我們可以如下去定義一個可處置對象類:

談談 .net 對象生命週期

可以看到,這個類中即有終結方法的重寫也有Dispose()方法,這樣就能保證:程序員若忘記調用Dispose()方法釋放非託管資源,那麼對象就會在垃圾回收的過程中調用終結方法來釋放非託管資源;


若程序員調用了Dispose()方法,那麼 GC.SuppressFinalize(this) 會保證在垃圾回收過程中不再會調用對象的終結方法,避免不必要的資源開銷。可謂“雙劍合璧”,保萬無一失。

話音剛落,c++程序員雙手死死拉住c#程序員的褲子,"師父,收我為徒吧!我也要學c#…",c#程序員不想自己的褲子被扯破,於是答應了他。掏出一本上古神書…

談談 .net 對象生命週期

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

談談 .net 對象生命週期



分享到:


相關文章: