.NET基础 (05)内存管理和垃圾回收
內存管理和垃圾回收
1 簡述.NET中堆棧和堆的特點和差異
2 執行string abc="aaa"+"bbb"+"ccc"共分配了多少內存
3 .NET中GC的運行機制
4 Dispose方法和Finalize方法在何時被調用
5 GC中代(Generation)是什么,一共分幾代
6 GC機制中如何判斷一個對象是否仍在被使用
7 .NET的托管堆中是否可能出現內存泄漏現象
?
內存管理和垃圾回收
1 簡述.NET中堆棧和堆的特點和差異
每一個.NET程序都最終會運行在一個操作系統中,假設這個操作系統是傳統的32位操作系統,那么每個.NET程序都可以擁有一個4GB的虛擬內存。.NET會在這個4GB的內存塊中開辟出3塊內存分別作為堆棧、受托管的堆和非托管的堆。
.NET中的堆棧
.NET中的堆棧用來存儲值類型的對象和引用類型對象的引用,堆棧的分配是連續的,在.NET程序中,始終存儲了一個特殊的指針指向堆棧的尾部,這樣一個堆棧內存的分配就直接從這個指針指向的內存位置開始向下分配。
堆棧上的地址從高位開始往低位分配內存,.NET只需要保存一個堆棧指針指向下一個未分配的內存地址。對于所需要分配的對象,依次分配到堆棧中,其釋放也完全按照棧的邏輯,依次進行退棧。這里提到的“依次”,是指按照變量的作用域進行的。
ClassA a = new ClassA();a.inta = 1;a.intb = 2;這里假設ClassA是引用類型,則堆棧中依次需要分配a的引用、a.inta和a.intb。當a的作用域結束后,這3個變量則從堆棧中依次退出:a.intb、a.inta,然后才是a。
.NET中的托管堆
.NET中引用類型的對象是分配到托管堆上的。通常我們稱.NET中的堆,指的就是托管堆。托管堆也是進程內存空間中的一塊區域。托管堆的分配也是連續的。但是堆中存在暫時不能被分配卻已經無用的對象內存塊。當一個引用類型對象初始化時,就會通過堆上可用空間的指針分配一塊連續的內存,然后使用堆棧上的引用指向堆上的這塊內存塊。
程序通過分配在堆棧上的引用來找到分配到托管堆上的對象實例。當堆棧中的引用退出作用域時,就僅僅斷開引用和實際對象的聯系。而當托管堆中的內存不夠時,.NET開始執行垃圾回收。垃圾回收是一個非常復雜的過程,它不僅涉及托管堆中對象的釋放,而且需要引動合并托管堆中的內存塊。當垃圾回收后,堆內不被使用的對象才會被部分釋放,而在這之前,它們在堆內是暫時不可用的。
.NET中的非托管堆
所有需要分配內存的非托管資源將會被分配到非托管堆上。非托管堆需要程序員用指針手動地分配并且手動釋放。.NET的垃圾回收和內存管理制度不適用于非托管堆。
堆棧、托管堆、非托管堆的比較
堆棧的內存是連續分配的,按照作用域依次分配和釋放。.NET依靠一個堆棧指針就可以進行內存操作,分配一個對象和釋放一個對象的大部分操作就是自增或者自減堆棧指針。.NET中值類型對象和應用類型對象的引用是分配在堆棧中的。
托管堆的內存分配也是連續的,但它比堆棧復雜的多。一塊內存分配需要涉及很多.NET內存管理機制的內部操作,另外當內存不夠時,垃圾回收的代價也是非常大的。相對于堆棧,堆的分配效率低很多。.NET中引用類型對象是分配到托管堆上的,這些對象通過分配到堆棧上的引用來進行訪問。
非托管堆和托管堆的區別在于非托管堆不受.NET的管理。非托管堆的內存是由程序員手動分配和釋放的,垃圾回收機制不使用于非托管堆,內存塊也不會被合并移動,所以非托管堆的內存分配是按塊的、不連續的。
2 執行string abc="aaa"+"bbb"+"ccc"共分配了多少內存
它在堆棧上分配了一個存儲字符串引用的內存塊,并在托管堆上分配了一塊用以存儲“aaabbbccc”這個字符串對象的內存塊。
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();}?
對應的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的運行機制
?垃圾回收是指釋放托管堆上不再被使用的對象內存。其過程包括:通過算法找到不再被使用的對象、移動對象是所有仍在被使用的對象緊靠托管堆的一邊和調整各個狀態變量。
垃圾回收的運行成本很高,對性能的影響較大。程序員在編寫.NET代碼時,應該避免不必要的內存分配,盡量減少或避免使用GC.Collect來執行垃圾回收。
4 Dispose方法和Finalize方法在何時被調用
?實現了Dispose方法不能得到任何有關釋放的保證,Dispose方法的調用依賴于類型的使用者。當類型被不恰當的使用,Dispose方法將不會被調用,但using語法的存在幫助了類型Dispose方法的調用。
由于Dispose方法的調用依賴于使用者,為了彌補這一缺陷,.NET同時提供了Finalize方法。Finalize方法在GC執行垃圾回收時調用,具體機制如下:
- 當每個包含Finalize方法的類型的實例對象被分配時,.NET會在一張特定的表結構中添加一個引用并且指向這個實例對象。方便起見稱為“帶析構對象表”。
- 當GC執行并且檢測到一個不被使用的對象是,需要進一步檢查“帶析構對象表”來查看該對象類型是否有Finalize方法,如果沒有則該對象被視為垃圾,如果存在Finalize方法,則把該對象的引用從“帶析構對象表”移到另外一張表中,這里暫時稱它為“等待析構表”。并且該對象實例被視為仍在被使用。
- CLR將有一個單獨的線程負責處理“等待析構表”,其方法就是依次通過引用調用其中每個對象的Finalize方法,然后刪除引用,這時托管堆中的對象實例將處于不再被使用的狀態。
- 下一個GC執行時,將釋放已經被調用Finalize方法的那些對象實例。
Dispose和Finalize方法都是為了釋放對象中的非托管資源。
Dispose方法被使用者主動調用,而Finalize方法在對象被垃圾回收的第一輪回收后,由一個專用.NET線程進行調用。Dispose方法不能保證被執行,而.NET的垃圾回收機制保證了擁有Finalize方法并且需要被調用的類型對象的Finalize方法被執行。調用Finalize方法性能代價非常高,程序員可以通過GC.SupressFinalize方法通知.NET對象的Finalize方法不需要被調用。
5 GC中代(Generation)是什么,一共分幾代
?垃圾回收按照對象不被使用的可能性把托管堆內的對象分為3代:0代、1代、2代。越小的代擁有越多的釋放機會,CLR每執行n次0代回收,才會執行1次1代回收,每執行n次1代回收,才執行1次2代回收,而每一次GC中任存活的對象實例將被移到下一代上。
6 GC機制中如何判斷一個對象是否仍在被使用
?當沒有任何引用指向堆中的某個對象的實例時,這個對象就被視為不再使用。
垃圾回收機制把引用分為以下2類:
根引用:往往指那些靜態字段的引用,或者存活的局部變量的引用。
非根引用:指那些不屬于根引用的引用,往往是對象實例中的字段。
垃圾回收時,GC從所有仍在使用的根引用出發遍歷所有對象實例,那些不能遍歷到的對象將被視為不再被使用而進行回收。
查看下面代碼:
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();//靜態變量Employee a=new Employee();//局部變量Employee b=new Employee();//局部變量staticEmployee._boss=new Employee();//實例成員 Console.Read();Console.WriteLine(a);} }
?
代碼中擁有兩個局部變量和一個靜態變量,這些引用都是根引用。其中一個局部變量a擁有一個成員實例對象,這個引用就是一個非根引用。當代碼執行到Console.Read()時,存活的根引用有staticEmployee和a,前者是因為它是一個公共靜態變量,后這是因為后續代碼任然使用a。通過這兩個存活的引用GC會找到一個非根引用staticEmployee._boss,并且發現3個仍然存活的對象。而b的對象則被視為不再使用而被釋放。
這里GCzzz偵測出b引用不再被使用從而釋放了b對象,更簡單地確保b對象被視為不再被使用的方法是把b引用置null,即b=null。
當一個從根引用出發遍歷抵達一個已經被視為使用的對象時,將結束這一分支的遍歷,這樣做是為了避免死循環。
7 .NET的托管堆中是否可能出現內存泄漏現象
?.NET托管堆可能出現嚴重的內存泄露現象,主要原因有:大對象的頻繁分配和釋放、不恰當地保留根引用和錯誤的Finalize方法。
?大對象的分配
.NET中的大對象被分配在托管堆的一個特殊的區域,這里暫時稱呼它為“大對象堆”。在回收大對象堆內的對象時,其他的大對象不會被移動,這是考慮到大規模地移動對象需要耗費過的資源。這樣,程序過多的分配和釋放大對象后,會產生很多內存碎片。程序員應該盡量減少大對象的分配次數,尤其是那些作為局部變量的,將被大規模分配和釋放的大對象,典型的例子就是String類型。
不恰當地保存根引用
最常見的錯誤就是把一個對象申明為公共靜態變量,一個公共靜態變量將一直被GC視為一個在使用的根引用。當這個對象內部還包含更多的對象引用是,這些對象同樣不會被釋放。這里只是從性能方面考慮問題,在實際設計時還要考慮程序的架構和可擴展性。
不正確的使用Finalize方法
Finalize方法應該只致力于快速而簡單地釋放非托管資源,并且盡可能快地返回。不正確的Finalize方法可能包含這樣的代碼:
- 沒有保護地寫文件日志
- 訪問數據庫
- 訪問網絡
- 把當前對象賦給某個存活的引用
?
?
?
轉載請注明出處:
作者:JesseLZJ
出處:http://jesselzj.cnblogs.com
轉載于:https://www.cnblogs.com/jesselzj/p/4790225.html
總結
以上是生活随笔為你收集整理的.NET基础 (05)内存管理和垃圾回收的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 在最长的距离二叉树结点
- 下一篇: java 工厂的变形模拟的各种应用