談談.net物件生命週期
不用程式設計師操心的堆 — 託管堆
程式在計算機上跑著,就難免會佔用記憶體資源來儲存在程式執行過程中的資料,我們按照記憶體資源的存取方式將記憶體劃分為堆記憶體和棧記憶體。
棧記憶體,通常使用的場景是:對存取速度要求較高且資料量不大。
典型的棧記憶體使用的例子就是函式棧,每一個函式被呼叫時都會被分配一塊記憶體,這塊記憶體被稱為棧記憶體,以先進後出的方式存取資料,在函式執行過程中不斷往函式棧中壓入(PUSH)資料(值型別資料:int、float、物件的引用...),函式執行完後又將函式棧中的資料逐個彈出(POP),由於是以操作棧的形式來存取,所以訪問速度快。
堆記憶體,從字面意思上理解就好像是倉庫裡面可以存一堆破爛,你若是需要存點什麼東西就儘管往裡面一扔,倉庫裡有的是空間。事實確實也是如此,堆記憶體中可以存放大規格的資料(比如物件資源),這些資料是不適合存放在棧中的,因為棧空間的容量有限,這就是堆記憶體相對於棧記憶體的好處:容量大。但是它的缺點也是顯而易見的,那就是存取堆記憶體的資料相較於存取棧記憶體是非常慢的,試想一下,讓你在倉庫裡的一堆破爛裡去找你想要的東西是什麼感覺。
(棧記憶體比堆記憶體詳細參考:https://blog.csdn.net/boyxiaolong/article/details/8543676)
從記憶體分配方式上看,堆記憶體不同於棧記憶體,函式棧是在每一個函式被執行的時候被自動分配並且函式執行完成後自動回收,而如果你想使用堆記憶體,就得自己動手豐衣足食。
所以你會看到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#程式設計師停了下來,喝了口保溫杯裡的枸杞紅棗大補茶