引言
本篇文章主要介紹.NET中6個重要的概念:棧,堆,實值型別,參考型別,裝箱,拆箱。文章開始介紹當你聲明一個變數時,編譯器內部發生了什麼,然後介紹兩個重要的概念:棧和堆;最後介紹實值型別和參考型別,並說明一些有關它們的重要原理。
最後通過一個簡單的範例程式碼說明裝箱拆箱帶來的效能損耗。
聲明變數的內部機制
在.NET程式中,當你聲明一個變數,將在記憶體中分配一塊記憶體。這塊記憶體分為三部分:1,變數名;2,變數類型;3,變數值。
揭示了聲明一個變數時的內部機制,其中分配的記憶體類型依據你的變數類型。.NET中有兩種類型的記憶體:棧記憶體和堆記憶體。在接下來的內容中,我們會瞭解到這兩種類型的詳細內容。
棧和堆
為了明白什麼是棧和堆,先讓我們看下下面範例程式碼的內部機制:
public void Method1(){// Line 1int i=4;// Line 2int y=2;//Line 3class1 cls1 = new class1();}
這裡一共有3行代碼。讓我們一下逐行看一下它們是如何執行的:
第1行:當這行代碼執行時,編譯器為它分配一小塊棧記憶體。運行時棧負責提供者所需的記憶體;
第2行:程式繼續執行。如同名字一樣,棧在第一塊記憶體的頂部分配了一塊記憶體。你也可以認為是模組或零件一塊一塊疊起來;
記憶體的分配與釋放遵循後進先出(後進先出)邏輯,換句話說,記憶體只能在樣本中i記憶體塊的頂部分配或釋放。
第3行:在第3行,我們建立了一個對象。當該行執行時,編譯器在站上建立了一個指標,真實的Object Storage Service在另一種叫“堆”的記憶體中。"堆"並不跟蹤運行記憶體,它更像一堆隨時可以訪問的對象。堆用於動態分配記憶體。這裡需要著重說明的是引用指標是分配在棧上。聲明Class1 cls1時並不會給Class1的執行個體分配記憶體,而是分配一個棧變數cls1(並設定為null),然後把它指向“堆”。
退出方法:當方法退出時,它釋放了棧上所有記憶體變數。換句話說,棧上所有的"Int"變數都依據後進先出的邏輯被釋放掉了。要注意,此時不會釋放堆記憶體,這種記憶體稍後會被“垃圾收集器”釋放。
現在可能會有很多朋友奇怪為什麼要分配2種記憶體,而不是僅用一種記憶體。
如果仔細觀察,你會發現基本類型並不複雜,他們值包含簡單的值,如i=0。對象資料類型很複雜,它們會引用其它對象或基本類型。換句話說,它要保持其它多種多樣的引用,而每種類型必須存在記憶體中。物件類型需要動態記憶體而基本類型需要靜態記憶體。如果需要分配動態記憶體,那麼就分配到堆上;反之在棧上。
實值型別與參考型別
現在我們明白了棧和堆,接下來看實值型別和參考型別。實值型別的資料和記憶體在同一個位置,而參考型別是一個指向記憶體的指標。
下面樣本是一個整形資料類型變數i被賦給另一個整形資料類型變數j。它們的記憶體值都分配在棧上。當我們把一個int值分配給另外一個int值時,需要建立一個完全不同的拷貝。換句話說,你可以改變其中任何一個而不會影響另外一個。這種資料類型被稱為實值型別。 當我們建立一個對象,並把一個對象賦給另外一個對象時,它們的指標指向相同的記憶體(如,當我們把obj賦給obj1時,它們指向相同的記憶體)。換句話說,我們改變其中一個,會影響到另外一個,這種類型稱為參考型別。
那麼那種類型是實值型別和參考型別呢?
在.NET中,依據資料類型,變數被分配到堆或棧上。“string”和"Object"是參考型別,其他基本類型被分配到棧上,是實值型別,如:
裝箱與拆箱
通過上面學習,我們學到了很多有用的東西,其中最有用的是明白了當把資料從棧移動到堆上時會有效能損失。如執行個體,當我們把一個實值型別裝箱為參考型別時,資料從棧移動到堆上。反之,資料從堆移動到棧上。這種在堆和棧之間的移動帶來了效能的損失。資料從實值型別轉變為參考型別的過程稱為“裝箱”,反之為“拆箱”。
如果編譯上面的代碼,在ILDASM中看IL代碼就會發下如何進行裝箱拆箱操作的,如下:
裝箱拆箱的效能影響
為了揭示裝箱拆箱如何影響效能,我們把下面代碼運行10000次。一個函數有裝箱操作,另一個只有簡單代碼。我們用簡單的計時器看它們的已耗用時間。裝箱函數耗時 3542 MS,無裝箱操作的耗時2477MS。這說明在實際項目中,除非必須,否則應避免裝箱,拆箱操作。
備忘:
最近在CodeProject上看到<6 important .NET concepts: - Stack, heap, Value types, reference types, boxing and Unboxing>一文,個人覺得非常好,所以就翻一下給不想看英文的同學。由於能力有限,翻譯的不好,望大家多多包涵。