.NET基础 (05)内存管理和垃圾回收
內(nèi)存管理和垃圾回收
1 簡述.NET中堆棧和堆的特點(diǎn)和差異
2 執(zhí)行string abc="aaa"+"bbb"+"ccc"共分配了多少內(nèi)存
3 .NET中GC的運(yùn)行機(jī)制
4 Dispose方法和Finalize方法在何時被調(diào)用
5 GC中代(Generation)是什么,一共分幾代
6 GC機(jī)制中如何判斷一個對象是否仍在被使用
7 .NET的托管堆中是否可能出現(xiàn)內(nèi)存泄漏現(xiàn)象
?
內(nèi)存管理和垃圾回收
1 簡述.NET中堆棧和堆的特點(diǎn)和差異
每一個.NET程序都最終會運(yùn)行在一個操作系統(tǒng)中,假設(shè)這個操作系統(tǒng)是傳統(tǒng)的32位操作系統(tǒng),那么每個.NET程序都可以擁有一個4GB的虛擬內(nèi)存。.NET會在這個4GB的內(nèi)存塊中開辟出3塊內(nèi)存分別作為堆棧、受托管的堆和非托管的堆。
.NET中的堆棧
.NET中的堆棧用來存儲值類型的對象和引用類型對象的引用,堆棧的分配是連續(xù)的,在.NET程序中,始終存儲了一個特殊的指針指向堆棧的尾部,這樣一個堆棧內(nèi)存的分配就直接從這個指針指向的內(nèi)存位置開始向下分配。
堆棧上的地址從高位開始往低位分配內(nèi)存,.NET只需要保存一個堆棧指針指向下一個未分配的內(nèi)存地址。對于所需要分配的對象,依次分配到堆棧中,其釋放也完全按照棧的邏輯,依次進(jìn)行退棧。這里提到的“依次”,是指按照變量的作用域進(jìn)行的。
ClassA a = new ClassA();a.inta = 1;a.intb = 2;這里假設(shè)ClassA是引用類型,則堆棧中依次需要分配a的引用、a.inta和a.intb。當(dāng)a的作用域結(jié)束后,這3個變量則從堆棧中依次退出:a.intb、a.inta,然后才是a。
.NET中的托管堆
.NET中引用類型的對象是分配到托管堆上的。通常我們稱.NET中的堆,指的就是托管堆。托管堆也是進(jìn)程內(nèi)存空間中的一塊區(qū)域。托管堆的分配也是連續(xù)的。但是堆中存在暫時不能被分配卻已經(jīng)無用的對象內(nèi)存塊。當(dāng)一個引用類型對象初始化時,就會通過堆上可用空間的指針分配一塊連續(xù)的內(nèi)存,然后使用堆棧上的引用指向堆上的這塊內(nèi)存塊。
程序通過分配在堆棧上的引用來找到分配到托管堆上的對象實(shí)例。當(dāng)堆棧中的引用退出作用域時,就僅僅斷開引用和實(shí)際對象的聯(lián)系。而當(dāng)托管堆中的內(nèi)存不夠時,.NET開始執(zhí)行垃圾回收。垃圾回收是一個非常復(fù)雜的過程,它不僅涉及托管堆中對象的釋放,而且需要引動合并托管堆中的內(nèi)存塊。當(dāng)垃圾回收后,堆內(nèi)不被使用的對象才會被部分釋放,而在這之前,它們在堆內(nèi)是暫時不可用的。
.NET中的非托管堆
所有需要分配內(nèi)存的非托管資源將會被分配到非托管堆上。非托管堆需要程序員用指針手動地分配并且手動釋放。.NET的垃圾回收和內(nèi)存管理制度不適用于非托管堆。
堆棧、托管堆、非托管堆的比較
堆棧的內(nèi)存是連續(xù)分配的,按照作用域依次分配和釋放。.NET依靠一個堆棧指針就可以進(jìn)行內(nèi)存操作,分配一個對象和釋放一個對象的大部分操作就是自增或者自減堆棧指針。.NET中值類型對象和應(yīng)用類型對象的引用是分配在堆棧中的。
托管堆的內(nèi)存分配也是連續(xù)的,但它比堆棧復(fù)雜的多。一塊內(nèi)存分配需要涉及很多.NET內(nèi)存管理機(jī)制的內(nèi)部操作,另外當(dāng)內(nèi)存不夠時,垃圾回收的代價(jià)也是非常大的。相對于堆棧,堆的分配效率低很多。.NET中引用類型對象是分配到托管堆上的,這些對象通過分配到堆棧上的引用來進(jìn)行訪問。
非托管堆和托管堆的區(qū)別在于非托管堆不受.NET的管理。非托管堆的內(nèi)存是由程序員手動分配和釋放的,垃圾回收機(jī)制不使用于非托管堆,內(nèi)存塊也不會被合并移動,所以非托管堆的內(nèi)存分配是按塊的、不連續(xù)的。
2 執(zhí)行string abc="aaa"+"bbb"+"ccc"共分配了多少內(nèi)存
它在堆棧上分配了一個存儲字符串引用的內(nèi)存塊,并在托管堆上分配了一塊用以存儲“aaabbbccc”這個字符串對象的內(nèi)存塊。
static void Main(string[] args){string str1 = "aaa" + "bbb" + "ccc";string str2 = "aaabbbccc";string str3 = "aaa" + "bbb" + 2.ToString();Console.WriteLine(str1);Console.WriteLine(str2);Console.WriteLine(str3);Console.ReadKey();}?
對應(yīng)的IL代碼:
.method private hidebysig static void Main(string[] args) cil managed {.entrypoint// 代碼大小 57 (0x39).maxstack 2.locals init ([0] string str1,[1] string str2,[2] string str3,[3] int32 CS$0$0000)IL_0000: ldstr "aaabbbccc"IL_0005: stloc.0IL_0006: ldstr "aaabbbccc"IL_000b: stloc.1IL_000c: ldstr "aaabbb"IL_0011: ldc.i4.2IL_0012: stloc.3IL_0013: ldloca.s CS$0$0000IL_0015: call instance string [mscorlib]System.Int32::ToString()IL_001a: call string [mscorlib]System.String::Concat(string,string)IL_001f: stloc.2IL_0020: ldloc.0IL_0021: call void [mscorlib]System.Console::WriteLine(string)IL_0026: ldloc.1IL_0027: call void [mscorlib]System.Console::WriteLine(string)IL_002c: ldloc.2IL_002d: call void [mscorlib]System.Console::WriteLine(string)IL_0032: call valuetype [mscorlib]System.ConsoleKeyInfo [mscorlib]System.Console::ReadKey()IL_0037: popIL_0038: ret } // end of method Program::Main?
可見C#編譯器將"aaa"+"bbb"+"ccc"編譯成功"aaabbbccc"。但是對于"aaa" + "bbb" + 2.ToString() 則編譯成了"aaabbb"和臨時變量CS$0$0000,還要對對臨時變量ToString(),最后還要合并。
3 .NET中GC的運(yùn)行機(jī)制
?垃圾回收是指釋放托管堆上不再被使用的對象內(nèi)存。其過程包括:通過算法找到不再被使用的對象、移動對象是所有仍在被使用的對象緊靠托管堆的一邊和調(diào)整各個狀態(tài)變量。
垃圾回收的運(yùn)行成本很高,對性能的影響較大。程序員在編寫.NET代碼時,應(yīng)該避免不必要的內(nèi)存分配,盡量減少或避免使用GC.Collect來執(zhí)行垃圾回收。
4 Dispose方法和Finalize方法在何時被調(diào)用
?實(shí)現(xiàn)了Dispose方法不能得到任何有關(guān)釋放的保證,Dispose方法的調(diào)用依賴于類型的使用者。當(dāng)類型被不恰當(dāng)?shù)氖褂?#xff0c;Dispose方法將不會被調(diào)用,但using語法的存在幫助了類型Dispose方法的調(diào)用。
由于Dispose方法的調(diào)用依賴于使用者,為了彌補(bǔ)這一缺陷,.NET同時提供了Finalize方法。Finalize方法在GC執(zhí)行垃圾回收時調(diào)用,具體機(jī)制如下:
- 當(dāng)每個包含F(xiàn)inalize方法的類型的實(shí)例對象被分配時,.NET會在一張?zhí)囟ǖ谋斫Y(jié)構(gòu)中添加一個引用并且指向這個實(shí)例對象。方便起見稱為“帶析構(gòu)對象表”。
- 當(dāng)GC執(zhí)行并且檢測到一個不被使用的對象是,需要進(jìn)一步檢查“帶析構(gòu)對象表”來查看該對象類型是否有Finalize方法,如果沒有則該對象被視為垃圾,如果存在Finalize方法,則把該對象的引用從“帶析構(gòu)對象表”移到另外一張表中,這里暫時稱它為“等待析構(gòu)表”。并且該對象實(shí)例被視為仍在被使用。
- CLR將有一個單獨(dú)的線程負(fù)責(zé)處理“等待析構(gòu)表”,其方法就是依次通過引用調(diào)用其中每個對象的Finalize方法,然后刪除引用,這時托管堆中的對象實(shí)例將處于不再被使用的狀態(tài)。
- 下一個GC執(zhí)行時,將釋放已經(jīng)被調(diào)用Finalize方法的那些對象實(shí)例。
Dispose和Finalize方法都是為了釋放對象中的非托管資源。
Dispose方法被使用者主動調(diào)用,而Finalize方法在對象被垃圾回收的第一輪回收后,由一個專用.NET線程進(jìn)行調(diào)用。Dispose方法不能保證被執(zhí)行,而.NET的垃圾回收機(jī)制保證了擁有Finalize方法并且需要被調(diào)用的類型對象的Finalize方法被執(zhí)行。調(diào)用Finalize方法性能代價(jià)非常高,程序員可以通過GC.SupressFinalize方法通知.NET對象的Finalize方法不需要被調(diào)用。
5 GC中代(Generation)是什么,一共分幾代
?垃圾回收按照對象不被使用的可能性把托管堆內(nèi)的對象分為3代:0代、1代、2代。越小的代擁有越多的釋放機(jī)會,CLR每執(zhí)行n次0代回收,才會執(zhí)行1次1代回收,每執(zhí)行n次1代回收,才執(zhí)行1次2代回收,而每一次GC中任存活的對象實(shí)例將被移到下一代上。
6 GC機(jī)制中如何判斷一個對象是否仍在被使用
?當(dāng)沒有任何引用指向堆中的某個對象的實(shí)例時,這個對象就被視為不再使用。
垃圾回收機(jī)制把引用分為以下2類:
根引用:往往指那些靜態(tài)字段的引用,或者存活的局部變量的引用。
非根引用:指那些不屬于根引用的引用,往往是對象實(shí)例中的字段。
垃圾回收時,GC從所有仍在使用的根引用出發(fā)遍歷所有對象實(shí)例,那些不能遍歷到的對象將被視為不再被使用而進(jìn)行回收。
查看下面代碼:
class Employee {public Employee _boss;public override string ToString(){if (_boss == null){return "沒有BOSS";}else{return "有一個BOOS";}} }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();//實(shí)例成員 Console.Read();Console.WriteLine(a);} }
?
代碼中擁有兩個局部變量和一個靜態(tài)變量,這些引用都是根引用。其中一個局部變量a擁有一個成員實(shí)例對象,這個引用就是一個非根引用。當(dāng)代碼執(zhí)行到Console.Read()時,存活的根引用有staticEmployee和a,前者是因?yàn)樗且粋€公共靜態(tài)變量,后這是因?yàn)楹罄m(xù)代碼任然使用a。通過這兩個存活的引用GC會找到一個非根引用staticEmployee._boss,并且發(fā)現(xiàn)3個仍然存活的對象。而b的對象則被視為不再使用而被釋放。
這里GCzzz偵測出b引用不再被使用從而釋放了b對象,更簡單地確保b對象被視為不再被使用的方法是把b引用置null,即b=null。
當(dāng)一個從根引用出發(fā)遍歷抵達(dá)一個已經(jīng)被視為使用的對象時,將結(jié)束這一分支的遍歷,這樣做是為了避免死循環(huán)。
7 .NET的托管堆中是否可能出現(xiàn)內(nèi)存泄漏現(xiàn)象
?.NET托管堆可能出現(xiàn)嚴(yán)重的內(nèi)存泄露現(xiàn)象,主要原因有:大對象的頻繁分配和釋放、不恰當(dāng)?shù)乇A舾煤湾e誤的Finalize方法。
?大對象的分配
.NET中的大對象被分配在托管堆的一個特殊的區(qū)域,這里暫時稱呼它為“大對象堆”。在回收大對象堆內(nèi)的對象時,其他的大對象不會被移動,這是考慮到大規(guī)模地移動對象需要耗費(fèi)過的資源。這樣,程序過多的分配和釋放大對象后,會產(chǎn)生很多內(nèi)存碎片。程序員應(yīng)該盡量減少大對象的分配次數(shù),尤其是那些作為局部變量的,將被大規(guī)模分配和釋放的大對象,典型的例子就是String類型。
不恰當(dāng)?shù)乇4娓?/strong>
最常見的錯誤就是把一個對象申明為公共靜態(tài)變量,一個公共靜態(tài)變量將一直被GC視為一個在使用的根引用。當(dāng)這個對象內(nèi)部還包含更多的對象引用是,這些對象同樣不會被釋放。這里只是從性能方面考慮問題,在實(shí)際設(shè)計(jì)時還要考慮程序的架構(gòu)和可擴(kuò)展性。
不正確的使用Finalize方法
Finalize方法應(yīng)該只致力于快速而簡單地釋放非托管資源,并且盡可能快地返回。不正確的Finalize方法可能包含這樣的代碼:
- 沒有保護(hù)地寫文件日志
- 訪問數(shù)據(jù)庫
- 訪問網(wǎng)絡(luò)
- 把當(dāng)前對象賦給某個存活的引用
?
?
?
轉(zhuǎn)載請注明出處:
作者:JesseLZJ
出處:http://jesselzj.cnblogs.com
轉(zhuǎn)載于:https://www.cnblogs.com/jesselzj/p/4790225.html
總結(jié)
以上是生活随笔為你收集整理的.NET基础 (05)内存管理和垃圾回收的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 在最长的距离二叉树结点
- 下一篇: java 工厂的变形模拟的各种应用