您好,登錄后才能下訂單哦!
.net中對象的生命周期有哪些?針對這個問題,這篇文章詳細介紹了相對應的分析和解答,希望可以幫助更多想解決這個問題的小伙伴找到更簡單易行的方法。
程序在計算機上跑著,就難免會占用內存資源來存儲在程序運行過程中的數據,我們按照內存資源的存取方式將內存劃分為堆內存和棧內存。
棧內存,通常使用的場景是:對存取速度要求較高且數據量不大。
典型的棧內存使用的例子就是函數棧,每一個函數被調用時都會被分配一塊內存,這塊內存被稱為棧內存,以先進后出的方式存取數據,在函數執行過程中不斷往函數棧中壓入(PUSH)數據(值類型數據:int、float、對象的引用...),函數執行完后又將函數棧中的數據逐個彈出(POP),由于是以操作棧的形式來存取,所以訪問速度快。
堆內存,從字面意思上理解就好像是倉庫里面可以存一堆破爛,你若是需要存點什么東西就盡管往里面一扔,倉庫里有的是空間。事實確實也是如此,堆內存中可以存放大規格的數據(比如對象資源),這些數據是不適合存放在棧中的,因為棧空間的容量有限,這就是堆內存相對于棧內存的好處:容量大。但是它的缺點也是顯而易見的,那就是存取堆內存的數據相較于存取棧內存是非常慢的,試想一下,讓你在倉庫里的一堆破爛里去找你想要的東西是什么感覺。
從內存分配方式上看,堆內存不同于棧內存,函數棧是在每一個函數被執行的時候被自動分配并且函數執行完成后自動回收,而如果你想使用堆內存,就得自己動手豐衣足食。
所以你會看到c語言程序員會這樣去使用堆內存:
int *p = (int*)malloc(sizeof(int)); //在堆內存中申請一塊字節數為int字節數的堆內存,并返回指向該內存區域的指針 *p = 10; free(p); //釋放堆內存資源
你還會看見c++程序員這樣寫:
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、垃圾回收器如何判斷一個對象什么時候不再需要?
2、垃圾回收器又在什么時候會執行垃圾清理的操作?
別急,帶著問題慢慢往下看。
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); }
關于.net中對象的生命周期有哪些問題的解答就分享到這里了,希望以上內容可以對大家有一定的幫助,如果你還有很多疑惑沒有解開,可以關注億速云行業資訊頻道了解更多相關知識。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。