一、基礎(chǔ)類型和語法
1.1 .NET中所有類型的基類是什么?
在.NET中所有的內(nèi)建類型都繼承自System.Object類型。在C#中,不需要顯示地定義類型繼承自System.Object,編譯器將自動地自動地為類型添加上這個繼承申明,以下兩行代碼的作用完全一致:
public class A { }
public class A : System.Object { }
1.2 值類型和引用類型的區(qū)別?
在.NET中的類型分為值類型和引用類型,它們各有特點,其共同點是都繼承自System.Object,但最明顯的區(qū)分標(biāo)準(zhǔn)卻是是否繼承自System.ValueType(System.ValueType繼承自System.Object),也就是說所有繼承自System.ValueType的類型是值類型,而其他類型都是引用類型。常用的值類型包括:結(jié)構(gòu)、枚舉、整數(shù)型、浮點型、布爾型等等;而在C#中所有以class關(guān)鍵字定義的類型都是引用類型。
PS:嚴(yán)格來講,System.Object作為所有內(nèi)建類型的基類,本身并沒有值類型和引用類型之分。但是System.Object的對象,具有引用類型的特點。這也是值類型在某些場合需要裝箱和拆箱操作的原因。
(1)賦值時的區(qū)別
這是值類型與引用類型最顯著的一個區(qū)別:值類型的變量直接將獲得一個真實的數(shù)據(jù)副本,而對引用類型的賦值僅僅是把對象的引用賦給變量,這樣就可能導(dǎo)致多個變量引用到一個對象實例上。
?。?)內(nèi)存分配的區(qū)別
引用類型的對象將會在堆上分配內(nèi)存,而值類型的對象則會在堆棧上分配內(nèi)存。堆??臻g相對有限,但是運行效率卻比堆高很多。
(3)繼承結(jié)構(gòu)的區(qū)別
由于所有的值類型都有一個共同的基類System.ValueType,因此值類型具有了一些引用類型所不具有的共同性質(zhì),比較重要的一點就是值類型的比較方法:Equals。所有的值類型已經(jīng)實現(xiàn)了內(nèi)容的比較(而不再是引用地址的比較),而引用類型沒有重寫Equals方法還是采用引用比較。
1.3 裝箱和拆箱的原理?
(1)裝箱:CLR需要做額外的工作把堆棧上的值類型移動到堆上,這個操作就被稱為裝箱。
?。?)拆箱:裝箱操作的反操作,把堆中的對象復(fù)制到堆棧中,并且返回其值。
裝箱和拆箱都意味著堆和堆??臻g的一系列操作,毫無疑問,這些操作的性能代價是很大的,尤其對于堆上空間的操作,速度相對于堆棧的操作慢得多,并且可能引發(fā)垃圾回收,這些都將大規(guī)模地影響系統(tǒng)的性能。因此,我們應(yīng)該避免任何沒有必要的裝箱和拆箱操作。
如何避免呢,首先分析裝箱和拆箱經(jīng)常發(fā)生的場合:
?、僦殿愋偷母袷交敵?/p>
?、赟ystem.Object類型的容器
對于第①種情況,我們可以通過下面的改動示例來避免:
int i = 10;
Console.WriteLine('The value is {0}', i.ToString());
對于第②種情況,則可以使用泛型技術(shù)來避免使用針對System.Object類型的容器,有效避免大規(guī)模地使用裝箱和拆箱:
ArrayList arrList = new ArrayList();
arrList.Add(0);
arrList.Add('1');
// 使用泛型數(shù)據(jù)結(jié)構(gòu)代替ArrayList
List
intList = new List (); intList.Add(1);
intList.Add(2);
1.4 struct和class的區(qū)別,struct適用于哪些場合?
首先,struct(結(jié)構(gòu))是值類型,而class(類)是引用類型,所有的結(jié)構(gòu)對象都分配在堆棧上,而所有的類對象都分配在堆上。
其次,struct與class相比,不具備繼承的特性,struct雖然可以重寫定義在System.Object中的虛方法,但不能定義新的虛方法和抽象方法。
最后,struct不能有無參數(shù)的構(gòu)造方法(class默認(rèn)就有),也不能為成員變量定義初始值。
public struct A
{
public int a = 1; // 這里不能編譯通過
}
結(jié)構(gòu)對象在構(gòu)造時必須被初始化為0,構(gòu)造一個全0的對象是指在內(nèi)存中為對象分配一個合適的空間,并且把該控件置為0。
如何使用struct or class?當(dāng)一個類型僅僅是原始數(shù)據(jù)的集合,而不需要復(fù)雜的操作時,就應(yīng)該設(shè)計為struct,否則就應(yīng)該設(shè)計為一個class。
1.5 C#中方法的參數(shù)傳遞有哪幾種方式?
(1)ref關(guān)鍵字:引用傳遞參數(shù),需要在傳遞前初始化;(ref 要求參數(shù)在傳入前被初始化)
(2)out關(guān)鍵字:引用傳遞參數(shù),需要在返回前初始化;(out 要求參數(shù)在方法返回前被初始化)
ref和out這兩個關(guān)鍵字的功能極其類似,都用來說明該參數(shù)以引用方式進(jìn)行傳遞。大家都知道,.NET的類型分為引用類型和值類型,當(dāng)一個方法參數(shù)是引用類型時,傳遞的本質(zhì)就是對象的引用。所以,這兩個關(guān)鍵字的作用都發(fā)生在值類型上。
?。?)params關(guān)鍵字:允許方法在定義時不確定參數(shù)的數(shù)量,這種形式非常類似數(shù)組參數(shù),但形式更加簡潔易懂。
But,params關(guān)鍵字的使用也有一定局限:當(dāng)一個方法申明了一個params參數(shù)后,就不允許在其后面再有任何其他參數(shù)。
例如下面一段代碼,定義了兩個完全相等的方法:NotParams和UseParams,使用由params修飾參數(shù)的方法時,可以直接把所有變量集合傳入而無須先申明一個數(shù)組對象。
class Program
{
static void Main(string[] args)
{
// params
string s = 'I am a string';
int i = 10;
double f = 2.3;
object[] par = new object[3] { s, i, f };
// not use params
NotParams(par);
// use params
UseParams(s, i, f);
Console.ReadKey();
}
// Not use params
public static void NotParams(object[] par)
{
foreach (var obj in par)
{
Console.WriteLine(obj);
}
}
// Use params
public static void UseParams(params object[] par)
{
foreach (var obj in par)
{
Console.WriteLine(obj);
}
}
}
1.6 淺復(fù)制和深復(fù)制的區(qū)別?
(1)淺復(fù)制:復(fù)制一個對象的時候,僅僅復(fù)制原始對象中所有的非靜態(tài)類型成員和所有的引用類型成員的引用。(新對象和原對象將共享所有引用類型成員的實際對象)
?。?)深復(fù)制:復(fù)制一個對象的時候,不僅復(fù)制所有非靜態(tài)類型成員,還要復(fù)制所有引用類型成員的實際對象。
下圖展示了淺復(fù)制和深復(fù)制的區(qū)別:
在.NET中,基類System.Object已經(jīng)為所有類型都實現(xiàn)了淺復(fù)制,類型所要做的就是公開一個復(fù)制的接口,而通常的,這個接口會由ICloneable接口來實現(xiàn)。ICloneable只包含一個方法Clone,該方法既可以被實現(xiàn)為淺復(fù)制也可以被實現(xiàn)為深復(fù)制,具體如何取舍則根據(jù)具體類型的需求決定。此外,在Sys-tem.Object基類中,有一個保護(hù)的MemeberwiseClone()方法,它便用于進(jìn)行淺度復(fù)制。所以,對于引用類型,要想實現(xiàn)淺度復(fù)制時,只需要調(diào)用這個方法就可以了:
public object Clone()
{
return MemberwiseClone();
}
下面的代碼展示了一個使用ICloneable接口提供深復(fù)制的簡單示例:
public class DeepCopy : ICloneable
{
public int i = 0;
public A a = new A();
public object Clone()
{
// 實現(xiàn)深復(fù)制-方式1:依次賦值和實例化
DeepCopy newObj = new DeepCopy();
newObj.a = new A();
newObj.a.message = this.a.message;
newObj.i = this.i;
return newObj;
}
public new object MemberwiseClone()
{
// 實現(xiàn)淺復(fù)制
return base.MemberwiseClone();
}
public override string ToString()
{
string result = string.Format('I的值為{0},A為{1}', this.i.ToString(), this.a.message);
return result;
}
}
public class A
{
public string message = '我是原始A';
}
public class Program
{
static void Main(string[] args)
{
DeepCopy dc = new DeepCopy();
dc.i = 10;
dc.a = new A();
DeepCopy deepClone = dc.Clone() as DeepCopy;
DeepCopy shadowClone = dc.MemberwiseClone() as DeepCopy;
// 深復(fù)制的目標(biāo)對象將擁有自己的引用類型成員對象
deepClone.a.message = '我是深復(fù)制的A';
Console.WriteLine(dc);
Console.WriteLine(deepClone);
Console.WriteLine();
// 淺復(fù)制的目標(biāo)對象將和原始對象共享引用類型成員對象
shadowClone.a.message = '我是淺復(fù)制的A';
Console.WriteLine(dc);
Console.WriteLine(shadowClone);
Console.ReadKey();
}
}
其執(zhí)行結(jié)果如下圖所示,可以清楚地看到對深復(fù)制對象的屬性的賦值不會影響原始對象,而淺復(fù)制則相反。
從上面的代碼中可以看到,在深復(fù)制的實現(xiàn)中,如果每個對象都要這樣去進(jìn)行深度復(fù)制就太麻煩了,可以利用序列化/反序列化來對對象進(jìn)行深度復(fù)制:先把對象序列化(Serialize)到內(nèi)存中,然后再進(jìn)行反序列化,通過這種方式來進(jìn)行對象的深度復(fù)制:
[Serializable]
public class DeepCopy : ICloneable
{
......
public object Clone()
{
// 實現(xiàn)深復(fù)制-方式1:依次賦值和實例化
//DeepCopy newObj = new DeepCopy();
//newObj.a = new A();
//newObj.a.message = this.a.message;
//newObj.i = this.i;
//return newObj;
// 實現(xiàn)深復(fù)制-方式2:序列化/反序列化
BinaryFormatter bf = new BinaryFormatter();
MemoryStream ms = new MemoryStream();
bf.Serialize(ms, this);
ms.Position = 0;
return bf.Deserialize(ms);
}
......
}
[Serializable]
public class A
{
public string message = '我是原始A';
}
PS:一般可被繼承的類型應(yīng)該避免實現(xiàn)ICloneable接口,因為這樣做將強(qiáng)制所有的子類型都需要實現(xiàn)ICloneable接口,否則將使類型的深復(fù)制不能覆蓋子類的新成員。
二、內(nèi)存管理和垃圾回收
2.1 .NET中棧和堆的差異?
每一個.NET應(yīng)用程序最終都會運行在一個OS進(jìn)程中,假設(shè)這個OS的傳統(tǒng)的32位系統(tǒng),那么每個.NET應(yīng)用程序都可以擁有一個4GB的虛擬內(nèi)存。.NET會在這個4GB的虛擬內(nèi)存塊中開辟三塊內(nèi)存作為 堆棧、托管堆 以及 非托管堆。
?。?).NET中的堆棧
堆棧用來存儲值類型的對象和引用類型對象的引用(地址),其分配的是一塊連續(xù)的地址,如下圖所示,在.NET應(yīng)用程序中,堆棧上的地址從高位向低位分配內(nèi)存,.NET只需要保存一個指針指向下一個未分配內(nèi)存的內(nèi)存地址即可。
對于所有需要分配的對象,會依次分配到堆棧中,其釋放也會嚴(yán)格按照棧的邏輯(FILO,先進(jìn)后出)依次進(jìn)行退棧。(這里的“依次”是指按照變量的作用域進(jìn)行的),假設(shè)有以下一段代碼:
TempClass a = new TempClass();
a.numA = 1;
a.numB = 2;
其在堆棧中的內(nèi)存圖如下圖所示:
這里TempClass是一個引用類型,擁有兩個整型的int成員,在棧中依次需要分配的是a的引用,a.numA和a.numB。當(dāng)a的作用域結(jié)束之后,這三個會按照a.numB→a.numA→a的順序依次退棧。
?。?).NET中的托管堆
眾所周知,.NET中的引用類型對象時分配在托管堆上的,和堆棧一樣,托管堆也是進(jìn)程內(nèi)存空間中的一塊區(qū)域。But,托管堆的內(nèi)存分配卻和堆棧有很大區(qū)別。受益于.NET內(nèi)存管理機(jī)制,托管堆的分配也是連續(xù)的(從低位到高位),但是堆中卻存在著暫時不能被分配卻已經(jīng)無用的對象內(nèi)存塊。
當(dāng)一個引用類型對象被初始時,會通過指向堆上可用空間的指針分配一塊連續(xù)的內(nèi)存,然后使堆棧上的引用指向堆上剛剛分配的這塊內(nèi)存塊。下圖展示了托管堆的內(nèi)存分配方式:
如上圖所示,.NET程序通過分配在堆棧中的引用來找到分配在托管堆的對象實例。當(dāng)堆棧中的引用退出作用域時,這時僅僅就斷開和實際對象實例的引用聯(lián)系。而當(dāng)托管堆中的內(nèi)存不夠時,.NET會開始執(zhí)行GC(垃圾回收)機(jī)制。GC是一個非常復(fù)雜的過程,它不僅涉及托管堆中對象的釋放,而且需要移動合并托管堆中的內(nèi)存塊。當(dāng)GC之后,堆中不再被使用的對象實例才會被部分釋放(注意并不是完全釋放),而在這之前,它們在堆中是暫時不可用的。在C/C++中,由于沒有GC,因此可以直接free/delete來釋放內(nèi)存。
?。?).NET中的非托管堆
.NET程序還包含了非托管堆,所有需要分配堆內(nèi)存的非托管資源將會被分配到非托管堆上。非托管的堆需要程序員用指針手動地分配和釋放內(nèi)存,.NET中的GC和內(nèi)存管理不適用于非托管堆,其內(nèi)存塊也不會被合并移動,所以非托管堆的內(nèi)存分配是按塊的、不連續(xù)的。因此,這也解釋了我們?yōu)楹卧谑褂梅峭泄苜Y源(如:文件流、數(shù)據(jù)庫連接等)需要手動地調(diào)用Dispose()方法進(jìn)行內(nèi)存釋放的原因。
2.2 執(zhí)行string abc='aaa'+'bbb'+'ccc'共分配了多少內(nèi)存?
這是一個經(jīng)典的基礎(chǔ)知識題目,它涉及了字符串的類型、堆棧和堆的內(nèi)存分配機(jī)制,因此被很多人拿來考核開發(fā)者的基礎(chǔ)知識功底。首先,我們都知道,判斷值類型的標(biāo)準(zhǔn)是查看該類型是否會繼承自System.ValueType,通過查看和分析,string直接繼承于System.Object,因此string是引用類型,其內(nèi)存分配會遵照引用類型的規(guī)范,也就是說如下的代碼將會在堆棧上分配一塊存儲引用的內(nèi)存,然后再在堆上分配一塊存儲字符串實例對象的內(nèi)存。
string a = 'edc';
現(xiàn)在再來看看string abc='aaa'+'bbb'+'ccc',按照常規(guī)的思路,字符串具有不可變性,大部分人會認(rèn)為這里的表達(dá)式會涉及很多臨時變量的生成,可能C#編譯器會先執(zhí)行'aaa'+'bbb',并且把結(jié)果值賦給一個臨時變量,再執(zhí)行臨時變量和'ccc'相加,最后把相加的結(jié)果再賦值給abc。But,其實C#編譯器比想象中要聰明得多,以下的C#代碼和IL代碼可以充分說明C#編譯器的智能:
// The first format
string first = 'aaa' + 'bbb' + 'ccc';
// The second format
string second = 'aaabbbccc';
// Display string
Console.WriteLine(first);
Console.WriteLine(second);
該C#代碼的IL代碼如下圖所示:
正如我們所看到的,string abc='aaa'+'bbb'+'ccc';這樣的表達(dá)式被C#編譯器看成一個完整的字符串'aaabbbccc',而不是執(zhí)行某些拼接方法,可以將其看作是C#編譯器的優(yōu)化,所以在本次內(nèi)存分配中只是在棧中分配了一個存儲字符串引用的內(nèi)存塊,以及在托管堆分配了一塊存儲'aaabbbccc'字符串對象的內(nèi)存塊。
那么,我們的常規(guī)思路在.NET程序中又是怎么體現(xiàn)的呢?我們來看一下一段代碼:
int num = 1;
string str = 'aaa' + num.ToString();
Console.WriteLine(str);
這里我們首先初始化了一個int類型的變量,其次初始化了一個string類型的字符串,并執(zhí)行 + 操作,這時我們來看看其對應(yīng)的IL代碼:
如上圖所示,在這段代碼中執(zhí)行 + 操作,會調(diào)用String的Concat方法,該方法需要傳入兩個string類型的參數(shù),也就產(chǎn)生了另一個string類型的臨時變量。換句話說,在此次內(nèi)存分配中,堆棧中會分配一個存儲字符串引用的內(nèi)存塊,在托管堆則分配了兩塊內(nèi)存塊,分別存儲了存儲'aaa'字符串對象和'1'字符串對象。
可能這段代碼還是不熟悉,我們再來看看下面一段代碼,我們就感覺十分親切熟悉了:
string str = 'aaa';
str += 'bbb';
str += 'ccc';
Console.WriteLine(str);
其對應(yīng)的IL代碼如下圖所示:
如圖可以看出,在拼接過程中產(chǎn)生了兩個臨時字符串對象,并調(diào)用了兩次String.Concat方法進(jìn)行拼接,就不用多解釋了。
2.3 簡要說說.NET中GC的運行機(jī)制
GC是垃圾回收(Garbage Collect)的縮寫,它是.NET眾多機(jī)制中最為重要的一部分,也是對我們的代碼書寫方式影響最大的機(jī)制之一。.NET中的垃圾回收是指清理托管堆上不會再被使用的對象內(nèi)存,并且移動仍在被使用的對象使它們緊靠托管堆的一邊。下圖展示了一次垃圾回收之后托管堆上的變化(這里僅僅為了說明,簡化了GC的執(zhí)行過程,省略了包含F(xiàn)inalize方法對象的處理以及大對象分配的特殊性):
如上圖所示,我們可以知道GC的執(zhí)行過程分為兩個基本動作:
?。?)一是找到所有不再被使用的對象:對象A和對象C,并標(biāo)記為垃圾;
(2)二是移動仍在被使用的對象:對象B和對象D。
這樣之后,對象A和對象C所占用的內(nèi)存空間就被騰空出來,以備下次分配的時候使用。
PS:通常情況下,我們不需要手動干預(yù)垃圾回收的執(zhí)行,不過CLR仍然提供了一個手動執(zhí)行垃圾回收的方法:GC.Collect()。當(dāng)我們需要在某一批對象不再使用并且及時釋放內(nèi)存的時候可以調(diào)用該方法來實現(xiàn)。But,垃圾回收的運行成本較高(涉及到了對象塊的移動、遍歷找到不再被使用的對象、很多狀態(tài)變量的設(shè)置以及Finalize方法的調(diào)用等等),對性能影響也較大,因此我們在編寫程序時,應(yīng)該避免不必要的內(nèi)存分配,也盡量減少或避免使用GC.Collect()來執(zhí)行垃圾回收。
2.4 Dispose和Finalize方法在何時被調(diào)用?
由于有了垃圾回收機(jī)制的支持,對象的析構(gòu)(或釋放)和C++有了很大的不同,這就需要我們在設(shè)計類型的時候,充分理解.NET的機(jī)制,明確怎樣利用Dispose方法和Finalize方法來保證一個對象正確而高效地被析構(gòu)。
?。?)Dispose方法
// 摘要:
// 定義一種釋放分配的資源的方法。
[ComVisible(true)]
public interface IDisposable
{
// 摘要:
// 執(zhí)行與釋放或重置非托管資源相關(guān)的應(yīng)用程序定義的任務(wù)。
void Dispose();
}
Microsoft考慮到很多情況下程序員仍然希望在對象不再被使用時進(jìn)行一些清理工作,所以.NET提供了IDispose接口并且在其中定義了Dispose方法。通常我們會在Dispose方法中實現(xiàn)一些托管對象和非托管對象的釋放以及業(yè)績業(yè)務(wù)邏輯的結(jié)束工作等等。
But,即使我們實現(xiàn)了Dispose方法,也不能得到任何有關(guān)釋放的保證,Dispose方法的調(diào)用依賴于類型的使用者,當(dāng)類型被不恰當(dāng)?shù)厥褂?,Dispose方法將不會被調(diào)用。因此,我們一般會借助using等語法來幫助Dispose方法被正確調(diào)用。
?。?)Finalize方法
剛剛提到Dispose方法的調(diào)用依賴于類型的使用者,為了彌補這一缺陷,.NET還提供了Finalize方法。Finalize方法類似于C++中的析構(gòu)函數(shù)(方法),但又和C++的析構(gòu)函數(shù)不同。Finalize在GC執(zhí)行垃圾回收時被調(diào)用,其具體機(jī)制如下:
?、佼?dāng)每個包含F(xiàn)inalize方法的類型的實例對象被分配時,.NET會在一張?zhí)囟ǖ谋斫Y(jié)構(gòu)中添加一個引用并且指向這個實例對象,暫且稱該表為“帶析構(gòu)方法的對象表”;
?、诋?dāng)GC執(zhí)行并且檢測到一個不被使用的對象時,需要進(jìn)一步檢查“帶析構(gòu)方法的對象表”來查詢該對象類型是否含有Finalize方法,如果沒有則將該對象視為垃圾,如果存在則將該對象的引用移動到另外一張表,暫且稱其為“待析構(gòu)的對象表”,并且該對象實例仍然被視為在被使用。
③CLR將有一個單獨的線程負(fù)責(zé)處理“待析構(gòu)的對象表”,其執(zhí)行方法內(nèi)部就是依次通過調(diào)用其中每個對象的Finalize方法,然后刪除引用,這時托管堆中的對象實例就被視為不再被使用。
?、芟乱粋€GC執(zhí)行時,將釋放已經(jīng)被調(diào)用Finalize方法的那些對象實例。
?。?)結(jié)合使用Dispose和Finalize方法:標(biāo)準(zhǔn)Dispose模式
Finalize方法由于有CLR保證調(diào)用,因此比Dispose方法更加安全(這里的安全是相對的,Dispose需要類型使用者的及時調(diào)用),但在性能方面Finalize方法卻要差很多。因此,我們在類型設(shè)計時一般都會使用標(biāo)準(zhǔn)Dispose模式:Finalize方法作為Dispose方法的后備,只有在使用者沒有調(diào)用Dispose方法的情況下,F(xiàn)inalize方法才被視為需要執(zhí)行。這一模式保證了對象能夠被高效和安全地釋放,已經(jīng)被廣泛使用。
下面的代碼則是實現(xiàn)這種標(biāo)準(zhǔn)Dispose模式的一個模板:
public class BaseTemplate : IDisposable
{
// 標(biāo)記對象是否已經(jīng)被釋放
private bool isDisposed = false;
// Finalize方法
~BaseTemplate()
{
Dispose(false);
}
// 實現(xiàn)IDisposable接口的Dispose方法
public void Dispose()
{
Dispose(true);
// 告訴GC此對象的Finalize方法不再需要被調(diào)用
GC.SuppressFinalize(this);
}
// 虛方法的Dispose方法做實際的析構(gòu)工作
protected virtual void Dispose(bool isDisposing)
{
// 當(dāng)對象已經(jīng)被析構(gòu),則不必再繼續(xù)執(zhí)行
if(isDisposed)
{
return;
}
if(isDisposing)
{
// Step1:在這里釋放托管資源
}
// Step2:在這里釋放非托管資源
// Step3:最后標(biāo)記對象已被釋放
isDisposed = true;
}
public void MethodA()
{
if(isDisposed)
{
throw new ObjectDisposedException('對象已經(jīng)釋放');
}
// Put the logic code of MethodA
}
public void MethodB()
{
if (isDisposed)
{
throw new ObjectDisposedException('對象已經(jīng)釋放');
}
// Put the logic code of MethodB
}
}
public sealed class SubTemplate : BaseTemplate
{
// 標(biāo)記子類對象是否已經(jīng)被釋放
private bool disposed = false;
protected override void Dispose(bool isDisposing)
{
// 驗證是否已被釋放,確保只被釋放一次
if(disposed)
{
return;
}
if(isDisposing)
{
// Step1:在這里釋放托管的并且在這個子類型中申明的資源
}
// Step2:在這里釋放非托管的并且這個子類型中申明的資源
// Step3:調(diào)用父類的Dispose方法來釋放父類中的資源
base.Dispose(isDisposing);
// Step4:設(shè)置子類的釋放標(biāo)識
disposed = true;
}
}
真正做釋放工作的只是受保護(hù)的虛方法Dispose,它接收一個bool參數(shù),主要用于區(qū)分調(diào)用者是類型的使用者還是.NET的GC機(jī)制。兩者的區(qū)別在于通過Finalize方法釋放資源時不能再釋放或使用對象中的托管資源,這是因為這時的對象已經(jīng)處于不被使用的狀態(tài),很有可能其中的托管資源已經(jīng)被釋放掉了。在Dispose方法中GC.SuppressFinalize(this)告訴GC此對象在被回收時不需要調(diào)用Finalize方法,這一句是改善性能的關(guān)鍵,記住實現(xiàn)Dispose方法的本質(zhì)目的就在于避免所有釋放工作在Finalize方法中進(jìn)行。
2.5 GC中代(Generation)是什么,分為幾代?
在.NET的GC執(zhí)行垃圾回收時,并不是每次都掃描托管堆內(nèi)的所有對象實例,這樣做太耗費時間而且沒有必要。相反,GC會把所有托管堆內(nèi)的對象按照其已經(jīng)不再被使用的可能性分為三類,并且從最有可能不被使用的類別開始掃描,.NET對這樣的分類類別有一個稱呼:代(Generation)。
GC會把所有的托管堆內(nèi)的對象分為0代、1代和2代:
第0代,新近分配在堆上的對象,從來沒有被垃圾收集過。任何一個新對象,當(dāng)它第一次被分配在托管堆上時,就是第0代。
第1代,經(jīng)歷過一次垃圾回收后,依然保留在堆上的對象。
第2代,經(jīng)歷過兩次或以上垃圾回收后,依然保留在堆上的對象。如果第2代對象在進(jìn)行完垃圾回收后空間仍然不夠用,則會拋出OutOfMemoryException異常。
對于這三代,我們需要知道的是并不是每次垃圾回收都會同時回收3個代的所有對象,越小的代擁有著越多被釋放的機(jī)會。
CLR對于代的基本算法是:每執(zhí)行N次0代的回收,才會執(zhí)行一次1代的回收,而每執(zhí)行N次1代的回收,才會執(zhí)行一次2代的回收。當(dāng)某個對象實例在GC執(zhí)行時被發(fā)現(xiàn)仍然在被使用,它將被移動到下一個代中上,下圖簡單展示了GC對三個代的回收操作。
根據(jù).NET的垃圾回收機(jī)制,0代、1代和2代的初始分配空間分別為256KB、2M和10M。說完分代的垃圾回收設(shè)計,也許我們會有疑問,為什么要這樣弄?其實分代并不是空穴來風(fēng)的設(shè)計,而是參考了這樣一個事實:
一個對象實例存活的時間越長,那么它就具有更大的機(jī)率去存活更長的時間。換句話說,最有可能馬上就不被使用的對象實例,往往是那些剛剛被分配的對象實例,而且新分配的對象實例通常都會被馬上大量地使用。這也解釋了為什么0代對象擁有最多被釋放的機(jī)會,并且.NET也只為0代分配了一塊只有256KB的小塊邏輯內(nèi)存,以使得0代對象有機(jī)會被全部放入處理器的緩存中去,這樣做的結(jié)果就是使用頻率最高并且最有可能馬上可以被釋放的對象實例擁有了最高的使用效率和最快的釋放速度。
因為一次GC回收之后仍然被使用的對象會被移動到更高的代上,因此我們需要避免保留已經(jīng)不再被使用的對象引用,將對象的引用置為null是告訴.NET該對象不需要再使用的最直接的方法。
在前面我們提到Finalize方法會大幅影響性能,通過結(jié)合對代的理解,我們可以知道:在帶有Finalize方法的對象被回收時,該對象會被視為正在被使用從而被留在托管堆中,且至少要等一個GC循環(huán)才能被釋放(為什么是至少一個?因為這取決于執(zhí)行Finalize方法的線程的執(zhí)行速度)。很明顯,需要執(zhí)行Finalize方法的那些對象實例,被真正釋放時最樂觀的情況下也已經(jīng)位于1代的位置上了,而如果它們是在1代上才開始釋放或者執(zhí)行Finalize方法的線程運行得慢了一點,那該對象就在第2代上才被釋放,相對于0代,這樣的對象實例在堆中存留的時間將長很多。
2.6 GC機(jī)制中如何判斷一個對象仍然在被使用?
在.NET中引用類型對象實例通常通過引用來訪問,而GC判斷堆中的對象是否仍然在被使用的依據(jù)也是引用。簡單地說:當(dāng)沒有任何引用指向堆中的某個對象實例時,這個對象就被視為不再使用。
在GC執(zhí)行垃圾回收時,會把引用分為以下兩類:
?。?)根引用:往往指那些靜態(tài)字段的引用,或者存活的局部變量的引用;
(2)非根引用:指那些不屬于根引用的引用,往往是對象實例中的字段。
垃圾回收時,GC從所有仍在被使用的根引用出發(fā)遍歷所有的對象實例,那些不能被遍歷到的對象將被視為不再被使用而進(jìn)行回收。我們可以通過下面的一段代碼來直觀地理解根引用和非根引用:
class Program
{
public static Employee staticEmployee;
static void Main(string[] args)
{
staticEmployee = new Employee(); // 靜態(tài)變量
Employee a = new Employee(); // 局部變量
Employee b = new Employee(); // 局部變量
staticEmployee.boss = new Employee(); // 實例成員
Console.ReadKey();
Console.WriteLine(a);
}
}
public class Employee
{
public Employee boss;
public override string ToString()
{
if(boss == null)
{
return 'No boss';
}
return 'One boss';
}
}
上述代碼中一共有兩個局部變量和一個靜態(tài)變量,這些引用都是根引用。而其中一個局部變量 a 擁有一個成員實例對象,這個引用就是一個非跟引用。下圖展示了代碼執(zhí)行到Console.ReadKey()這行代碼時運行垃圾回收時的情況。
從上圖中可以看出,在執(zhí)行到Console.ReadKey()時,存活的根引用有staticEmployee和a,前者因為它是一個公共靜態(tài)變量,而后者則因為后續(xù)代碼還會使用到a。通過這兩個存活的根引用,GC會找到一個非跟引用staticEmployee.boss,并且發(fā)現(xiàn)三個仍然存活的對象。而b的對象則將被視為不再使用從而被釋放。(更簡單地確保b對象不再被視為在被使用的方法時把b的引用置為null,即b=null;)
此外,當(dāng)一個從根引用觸發(fā)的遍歷抵達(dá)一個已經(jīng)被視為在使用的對象時,將結(jié)束這一個分支的遍歷,這樣做可以避免陷入死循環(huán)。
2.7 .NET中的托管堆中是否可能出現(xiàn)內(nèi)存泄露的現(xiàn)象?
首先,必須明確一點:即使在擁有垃圾回收機(jī)制的.NET托管堆上,仍然是有可能發(fā)生內(nèi)存泄露現(xiàn)象的。
其次,什么是內(nèi)存泄露?內(nèi)存泄露是指內(nèi)存空間上產(chǎn)生了不再被實際使用卻又不能被分配的內(nèi)存空間,其意義很廣泛,像內(nèi)存碎片、不徹底的對象釋放等都屬于內(nèi)存泄露現(xiàn)象。內(nèi)存泄露將導(dǎo)致主機(jī)的內(nèi)存隨著程序的運行而逐漸減少,無論其表現(xiàn)形式怎樣,它的危害是很大的,因此我們需要努力地避免。
按照內(nèi)存泄露的定義,我們可以知道在大部分的時候.NET中的托管堆中存在著短暫的內(nèi)存泄露情況,因為對象一旦不再被使用,需要等到下一個GC時才會被釋放。這里列舉幾個在.NET中常見的幾種對系統(tǒng)危害較大的內(nèi)存泄露情況,我們在實際開發(fā)中需要極力避免:
(1)大對象的分配
.NET中所有的大對象(這里主要是指對象的大小超過指定數(shù)值[85000字節(jié)])將分配在托管堆內(nèi)一個特殊的區(qū)域內(nèi),暫且將其稱為“大對象堆”(這也算是CLR對于GC的一個優(yōu)化策略)。大對象堆中最重要的一個特點就是:沒有代級的概念,所有對象都被視為第2代。在回收大對象堆內(nèi)的對象時,其他的大對象不會被移動,這是考慮到大規(guī)模地移動對象需要耗費過多的資源。這樣,在程序過多地分配和釋放大對象之后,就會產(chǎn)生很多內(nèi)存碎片。下圖解釋了這一過程:
如圖所示可以看出,隨著對象的分配和釋放不斷進(jìn)行,在不進(jìn)行對象移動的大對象堆內(nèi),將不可避免地產(chǎn)生小的內(nèi)存碎片。我們所需要做的就是盡量減少大對象的分配次數(shù),尤其是那些作為局部變量的,將被大規(guī)模分配和釋放的大對象,典型的例子就是String類型。
?。?)不恰當(dāng)?shù)乇4娓?/p>
最簡單的一個錯誤例子就是不恰當(dāng)?shù)匕岩粋€對象申明為公共靜態(tài)變量,一個公共的靜態(tài)變量將一直被GC視為一個在使用的根引用。更糟糕的是:當(dāng)這個對象內(nèi)部還包含更多的對象引用時,這些對象同樣不會被釋放。例如下面一段代碼:
public class Program
{
// 公共靜態(tài)大對象
public static RefRoot bigObject = new RefRoot('test');
public static void Main(string[] args)
{
Console.ReadKey();
}
}
public class RefRoot
{
// 這是一個占用大量內(nèi)存的成員
public string[] BigMember;
public RefRoot(string content)
{
// 初始化大對象
BigMember = new string[1000];
for (int i = 0; i < 1000;="">
{
BigMember[i] = content;
}
}
}
在代碼中,定義了一個公共靜態(tài)的大對象,這個對象將直到程序運行結(jié)束后才會被GC釋放掉。如果在整個程序中各個類型不斷地使用這個靜態(tài)成員,那這樣的設(shè)計有助于減少大對象堆內(nèi)的內(nèi)存碎片,但是如果整個程序極少地甚至只有一次使用了這個成員,那考慮到它占用的內(nèi)存會影響整體系統(tǒng)性能,設(shè)計時則應(yīng)該考慮設(shè)計成實例變量,以便GC能夠及時釋放它。
(3)不正確的Finalize方法
前面已經(jīng)介紹了Finalize方法時由GC的一個專用的線程進(jìn)行調(diào)用,拋開Microsoft怎樣實現(xiàn)的這個具體的調(diào)度算法,有一點可以肯定的是:不正確的Finalize方法將導(dǎo)致Finalize方法不能被正確執(zhí)行。如果系統(tǒng)中所有的Finalize方法不能被正確執(zhí)行,包含它們的對象也只能駐留在托管堆內(nèi)不能被釋放,這樣的情況將會導(dǎo)致嚴(yán)重的后果。
那么,什么是不正確的Finalize方法?Finalize方法應(yīng)該只致力于快速而簡單地釋放非托管資源,并且盡可能快地返回。相反,不正確的Finalize方法則可能包含以下這樣的一些代碼:
?、贈]有保護(hù)地寫文件日志;
?、谠L問數(shù)據(jù)庫;
?、墼L問網(wǎng)絡(luò);
?、馨旬?dāng)前對象賦給某個存活的引用;
例如,當(dāng)Finalize方法試圖訪問文件系統(tǒng)、數(shù)據(jù)庫或者網(wǎng)絡(luò)時,將會有資源爭用和等待的潛在危險。試想一個不斷嘗試訪問離線數(shù)據(jù)庫的Finalize方法,將會在長時間內(nèi)不會返回,這不僅影響了對象的釋放,也使得排在Finalize方法隊列中的所有后續(xù)對象得不到釋放,這個連鎖反應(yīng)將會導(dǎo)致很快地造成內(nèi)存耗盡。此外,如果在Finalize方法中把對象自身又賦給了另外一個存活的引用,這時對象內(nèi)的一部分資源已經(jīng)被釋放掉了,而另外一部分還沒有,當(dāng)這樣一個對象被激活后,將導(dǎo)致不可預(yù)知的后果。
參考資料
(1)朱毅,《進(jìn)入IT企業(yè)必讀的200個.NET面試題》
(2)張子陽,《.NET之美:.NET關(guān)鍵技術(shù)深入解析》
(3)王濤,《你必須知道的.NET》