盡管C#(事實上是基於.Net Framework的所有語言)自動處理了內存的分配和釋放的問題,並且引入了垃圾收集機制,有完善的數據類型管理能力。但是對於很多情況下,了解其深層的機制是非常有用的,能夠大大提高程序的效率。如今Phone7的發布,在移動設備和一些特殊應用上。聰明而又有技巧地處理內存管理和各種數據類型顯得非常有用,能夠更好得維護和開發程序。
1. Windows內存管理機制
各位要深入了解C#的內存管理機制,首先必須要先了解下Windows的內存管理系統。俺記得大學的時候開過一門課叫《操作系統原理》,大家別砸我,這門課俺烤得不好……hh, 關鍵是提一下內存管理系統,大家還記得塊映射和分頁映射不?CPU在高速緩存裡維護了一張表,放的是內存和磁盤上的數據塊的映射關系。具體我就不說了(書上弄了兩章來掰扯這個機制),反正就是把一個程序的內存空間分成很多塊,會有個算法把最近使用的內存塊寫入內存(RAM),內存塊還可以分,放進高速緩存(Cache),以便下次訪問能夠高速讀取。當然還涉及到寫的機制,有很多種方式,還有沖突處理,鎖什麼的,這些機制當年很令人抓狂哈,現在想想其實網站和程序的緩存機制不是就脫胎於這些算法嗎?只不過簡單了很多。在32位的系統下,一個程序的最大內存空間是4GB,這4GB就這麼在底層的操作系統下管理著,分成了很多段,頁和塊。這些內存塊裡放了些什麼東西呢?嗯,大家都知道的--數據和邏輯。邏輯就是你的代碼中的方法,表示計算,運行,處理;那數據就是要加工的對象了。在C++裡面分成簡單數據類型和指針數據類型。這個數據,包括變量,常量,對象實體。通常數據占據了一個程序絕大部分的內存空間。也是我們需要進行內存管理的部分。
2. C#數據類型
其實C#中的數據類型也是和C++一樣分兩種,分別是值類型和引用類型。
(1)值類型對應於C++的簡單數據類型,比如int,double,bool,值類型是.Net Framework的預定義類型,總共有13個,詳見下表: 類型 說明 注意
char 16位unicode字符
sbyte 8位有符號整型
short 16位有符號整型
int 32位有符號整型 數值默認變量類型
long 64位有符號整型 前綴:L
byte 8位無符號整型
ushort 16位無符號整型
unit 32位無符號整型 前綴:U
ulong 64位無符號整型 前綴:UL
float 32位單精浮點數 前綴:F
double 64位雙精浮點數 默認浮點數變量類型
decimal 128位高精度十進位數 會造成性能損失,前綴:M
bool bool值 true/false
*16進制數加前綴0x,如0x12ba;
還有一點必須指出,C#中的結構struct和C++不同,這是一個值類型,不能被繼承。struct派生於System.ValueType。
.Net的內置類型也叫CTS類型,值類型存儲的位置是內存空間的堆棧區(stack),《數據結構》裡的堆棧(先進後出)
view sourceprint?01 {
02 int p1;
03 int p2;
04 //code
05 {
06 int q1;
07 int q2;
08 //code
09 }
10 }
stack top 變量
700012-700015 p1
700008-700011 p2
700004-700007 q1
700000-700003 q2
stack bottom
可看到上圖所示,變量是外層括號先進棧,內層後進棧的順序。釋放時則反之。堆棧指針總是指向最後進棧的變量,同時保留指向下一個堆棧空位的指針。
這樣的結構會使得變量的讀取非常高效,只要移動指針就能獲得和釋放變量。缺點是不夠靈活,管理很大的數據結構效率不高。對於值類型,是不需要.net的垃圾回收機制的,本身就能很好地進行內存管理。
(2)引用類型對應另外一種C++的數據類型,指針類型。這就是說我們如果創建一個引用類型,那麼實際是在堆中分配了一塊內存空間。然後這個變量實際上是指向這個內存塊的指針,在C#中有兩種數據是作為引用類型的:第一類是CTS類型中的引用類型,有兩個:object和string; 第二類是C#中各種的類,必須從object繼承(包括自定義類和C#類庫)。廣義上講object類型加上string也對,因為所有C#(.Net Framwork)的類都是繼承於System.object這個基類的。
為什麼我們要把Object和String類型分開呢?這個是有原因的,string類型是個很特殊的引用類型。
view sourceprint?01 using System;
02
03 namespace StringTest
04 {
05 class StringExpress
06 {
07 public static void Main()
08 {
09 string s1 = "I am a string";
10 string s2 = s1;
11 Console.WriteLine(s1);
12 Console.WriteLine(s2);
13 s2 = "I am a new string";
14 Console.WriteLine(s1);
15 Console.WriteLine(s2);
16 }
17 }
18 }
既然是引用類型大家覺得通常情況下應該顯示什麼呢?因為變量只是個引用(指針),所以s1和s2應該是指向同一個內存區的,事實上,當運行
view sourceprint?1 string s2 = s1;
s1和s2的確是指向一樣的字符串變量的,前兩行輸出是:
I am a string
I am a string
大家猜猜後兩行會是什麼呢?結果是:
I am a string
I am a new string
這就是string類型的特殊之處了,當你給string賦一個新值時,將會創建新的對象而不會覆蓋原來的值,這實際上是應用了運算符重載,重新分配了引用對象。
Object類有很多應用和特點,會在以後的文章中討論。裝箱拆箱,還有一些對類基本方法也都是在Object中定義的。
3. 垃圾收集
垃圾回收是.Net中的一個非常重要的機制,事實上這個機制在很大程度上確保了.Net Framework的高性能。垃圾回收是一種對引用類型數據進行釋放的自動機制。在C++中這個是不存在的,所有的對象實體都要代碼明確得釋放,否則會一直占據內存空間。
我們知道引用類型,維護的是一個指針。這個指針本身實際上也是放在堆棧(Stack)中的,而指向的內存空間是在堆(heap)中。GC(垃圾收集)去釋放堆中的內存是根據引用指針來判斷地,如果堆中的一個內存塊已經沒有任何引用(引用變量已經超出范圍),那麼當一輪垃圾回收進行時,這塊內存會被釋放。事實上.Net的垃圾回收機制並不只是做了這些。通常堆中的數據,由於多次釋放和分配,會變得支離破碎。成為一塊塊的內存塊,這樣為分配新的內存空間造成麻煩。因為分配新的空間時需要遍歷整個堆空間,以確定一塊足夠創建對象實例的內存塊,並建立引用變量,同時維護堆的存儲記錄。這樣不但效率底下,而且會降低內存使用率。
GC在垃圾回收時會做兩個步驟,1.釋放內存空間。2.重新調整並壓縮堆區,使剩下的對象重新移動到堆的一個端部。盡管這樣做會造成一些額外的資源開銷,但是完成這個步驟後,實例化一個對象,以及查找訪問實例都會快很多,事實上提高了性能。這個第二步機制也是托管堆和未托管堆的主要區別。
垃圾收集的時機是由系統控制的,具體的算法微軟並沒有公布過。通常我們也可以自己在需要的時候調用垃圾回收機制,能夠顯式地執行System.GC.Collect()。但不推薦,因為這個過程事實上是比較消耗資源的,沒有自動收集效率高;對整個程序的執行效果來說,正常情況下還是使用系統的自動收集機制更好些。
下一篇文章將會詳細分析垃圾回收機制和C#的對象使用。