.NET基礎拾遺(1)類型語法基礎和內存管理基礎

 Index :html

(1)類型語法、內存管理和垃圾回收基礎程序員

(2)面向對象的實現和異常的處理面試

(3)字符串、集合與流算法

(4)委託、事件、反射與特性數據庫

(5)多線程開發基礎數組

(6)ADO.NET與數據庫開發基礎緩存

(7)WebService的開發與應用基礎安全

1、基礎類型和語法

1.1 .NET中全部類型的基類是什麼?

  在.NET中全部的內建類型都繼承自System.Object類型。在C#中,不須要顯示地定義類型繼承自System.Object,編譯器將自動地自動地爲類型添加上這個繼承申明,如下兩行代碼的做用徹底一致:網絡

    public class A { }
    public class A : System.Object { }

1.2 值類型和引用類型的區別?

  在.NET中的類型分爲值類型和引用類型,它們各有特色,其共同點是都繼承自System.Object,但最明顯的區分標準倒是是否繼承自System.ValueType(System.ValueType繼承自System.Object),也就是說全部繼承自System.ValueType的類型是值類型,而其餘類型都是引用類型。經常使用的值類型包括:結構、枚舉、整數型、浮點型、布爾型等等;而在C#中全部以class關鍵字定義的類型都是引用類型。數據結構

PS:嚴格來說,System.Object做爲全部內建類型的基類,自己並無值類型和引用類型之分。可是System.Object的對象,具備引用類型的特色。這也是值類型在某些場合須要裝箱和拆箱操做的緣由。

  (1)賦值時的區別

  這是值類型與引用類型最顯著的一個區別:值類型的變量直接將得到一個真實的數據副本,而對引用類型的賦值僅僅是把對象的引用賦給變量,這樣就可能致使多個變量引用到一個對象實例上

  (2)內存分配的區別

  引用類型的對象將會在堆上分配內存,而值類型的對象則會在堆棧上分配內存。堆棧空間相對有限,可是運行效率卻比堆高不少。

  (3)繼承結構的區別

  因爲全部的值類型都有一個共同的基類System.ValueType,所以值類型具備了一些引用類型所不具備的共同性質,比較重要的一點就是值類型的比較方法:Equals。全部的值類型已經實現了內容的比較(而再也不是引用地址的比較),而引用類型沒有重寫Equals方法仍是採用引用比較。

1.3 裝箱和拆箱的原理?

  (1)裝箱:CLR須要作額外的工做把堆棧上的值類型移動到堆上,這個操做就被稱爲裝箱。

  (2)拆箱:裝箱操做的反操做,把堆中的對象複製到堆棧中,而且返回其值。

  裝箱和拆箱都意味着堆和堆棧空間的一系列操做,毫無疑問,這些操做的性能代價是很大的,尤爲對於堆上空間的操做,速度相對於堆棧的操做慢得多,而且可能引起垃圾回收,這些都將大規模地影響系統的性能。所以,咱們應該避免任何沒有必要的裝箱和拆箱操做

  如何避免呢,首先分析裝箱和拆箱常常發生的場合:

  ①值類型的格式化輸出

  ②System.Object類型的容器

  對於第①種狀況,咱們能夠經過下面的改動示例來避免:

    int i = 10;
    Console.WriteLine("The value is {0}", i.ToString());

  對於第②種狀況,則可使用泛型技術來避免使用針對System.Object類型的容器,有效避免大規模地使用裝箱和拆箱:

    ArrayList arrList = new ArrayList();
    arrList.Add(0);
    arrList.Add("1");
    // 使用泛型數據結構代替ArrayList
    List<int> intList = new List<int>();
    intList.Add(1);
    intList.Add(2);

1.4 struct和class的區別,struct適用於哪些場合?

  首先,struct(結構)是值類型,而class(類)是引用類型,全部的結構對象都分配在堆棧上,而全部的類對象都分配在堆上。

  其次,struct與class相比,不具有繼承的特性,struct雖然能夠重寫定義在System.Object中的虛方法,但不能定義新的虛方法和抽象方法。

  最後,struct不能有無參數的構造方法(class默認就有),也不能爲成員變量定義初始值。

    public struct A
    {
        public int a = 1; // 這裏不能編譯經過
    }

  結構對象在構造時必須被初始化爲0,構造一個全0的對象是指在內存中爲對象分配一個合適的空間,而且把該控件置爲0。

  如何使用struct or class?當一個類型僅僅是原始數據的集合,而不須要複雜的操做時,就應該設計爲struct,不然就應該設計爲一個class

1.5 C#中方法的參數傳遞有哪幾種方式?

  (1)ref關鍵字:引用傳遞參數,須要在傳遞前初始化;(ref 要求參數在傳入前被初始化)

  (2)out關鍵字:引用傳遞參數,須要在返回前初始化;(out 要求參數在方法返回前被初始化)

  ref和out這兩個關鍵字的功能極其相似,都用來講明該參數以引用方式進行傳遞。你們都知道,.NET的類型分爲引用類型和值類型,當一個方法參數是引用類型時,傳遞的本質就是對象的引用。因此,這兩個關鍵字的做用都發生在值類型

  (3)params關鍵字:容許方法在定義時不肯定參數的數量,這種形式很是相似數組參數,但形式更加簡潔易懂。

  But,params關鍵字的使用也有必定侷限:當一個方法申明瞭一個params參數後,就不容許在其後面再有任何其餘參數

  例以下面一段代碼,定義了兩個徹底相等的方法:NotParams和UseParams,使用由params修飾參數的方法時,能夠直接把全部變量集合傳入而無須先申明一個數組對象。

    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);
            }
        }
    }
View Code

1.6 淺複製和深複製的區別?

  (1)淺複製:複製一個對象的時候,僅僅複製原始對象中全部的非靜態類型成員和全部的引用類型成員的引用。(新對象和原對象將共享全部引用類型成員的實際對象)

  (2)深複製:複製一個對象的時候,不只複製全部非靜態類型成員,還要複製全部引用類型成員的實際對象

  下圖展現了淺複製和深複製的區別:

  在.NET中,基類System.Object已經爲全部類型都實現了淺複製,類型所要作的就是公開一個複製的接口,而一般的,這個接口會由ICloneable接口來實現。ICloneable只包含一個方法Clone,該方法既能夠被實現爲淺複製也能夠被實現爲深複製,具體如何取捨則根據具體類型的需求決定。此外,在Sys-tem.Object基類中,有一個保護的MemeberwiseClone()方法,它便用於進行淺度複製。因此,對於引用類型,要想實現淺度複製時,只須要調用這個方法就能夠了:

    public object Clone()
    {    
        return MemberwiseClone();
    }

  下面的代碼展現了一個使用ICloneable接口提供深複製的簡單示例:

    public class DeepCopy : ICloneable
    {
        public int i = 0;
        public A a = new A();

        public object Clone()
        {
            // 實現深複製-方式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()
        {
            // 實現淺複製
            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;

            // 深複製的目標對象將擁有本身的引用類型成員對象
            deepClone.a.message = "我是深複製的A";
            Console.WriteLine(dc);
            Console.WriteLine(deepClone);
            Console.WriteLine();
            // 淺複製的目標對象將和原始對象共享引用類型成員對象
            shadowClone.a.message = "我是淺複製的A";
            Console.WriteLine(dc);
            Console.WriteLine(shadowClone);

            Console.ReadKey();
        }
    }
View Code

  其執行結果以下圖所示,能夠清楚地看到對深複製對象的屬性的賦值不會影響原始對象,而淺複製則相反。

  從上面的代碼中能夠看到,在深複製的實現中,若是每一個對象都要這樣去進行深度複製就太麻煩了,能夠利用序列化/反序列化來對對象進行深度複製:先把對象序列化(Serialize)到內存中,而後再進行反序列化,經過這種方式來進行對象的深度複製:

    [Serializable]
    public class DeepCopy : ICloneable
    {
        ......

        public object Clone()
        {
            // 實現深複製-方式1:依次賦值和實例化
            //DeepCopy newObj = new DeepCopy();
            //newObj.a = new A();
            //newObj.a.message = this.a.message;
            //newObj.i = this.i;

            //return newObj;
            // 實現深複製-方式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";
    }
View Code

PS:通常可被繼承的類型應該避免實現ICloneable接口,由於這樣作將強制全部的子類型都須要實現ICloneable接口,不然將使類型的深複製不能覆蓋子類的新成員。

2、內存管理和垃圾回收

2.1 .NET中棧和堆的差別?

  每個.NET應用程序最終都會運行在一個OS進程中,假設這個OS的傳統的32位系統,那麼每一個.NET應用程序均可以擁有一個4GB的虛擬內存。.NET會在這個4GB的虛擬內存塊中開闢三塊內存做爲 堆棧託管堆 以及 非託管堆

  (1).NET中的堆棧

  堆棧用來存儲值類型的對象和引用類型對象的引用(地址),其分配的是一塊連續的地址,以下圖所示,在.NET應用程序中,堆棧上的地址從高位向低位分配內存,.NET只須要保存一個指針指向下一個未分配內存的內存地址便可。

  對於全部須要分配的對象,會依次分配到堆棧中,其釋放也會嚴格按照棧的邏輯(FILO,先進後出)依次進行退棧。(這裏的「依次」是指按照變量的做用域進行的),假設有如下一段代碼:

    TempClass a = new TempClass();
    a.numA = 1;
    a.numB = 2;

  其在堆棧中的內存圖以下圖所示:

  這裏TempClass是一個引用類型,擁有兩個整型的int成員,在棧中依次須要分配的是a的引用,a.numA和a.numB。當a的做用域結束以後,這三個會按照a.numB→a.numA→a的順序依次退棧。

  (2).NET中的託管堆

  衆所周知,.NET中的引用類型對象時分配在託管堆上的,和堆棧同樣,託管堆也是進程內存空間中的一塊區域。But,託管堆的內存分配卻和堆棧有很大區別。受益於.NET內存管理機制,託管堆的分配也是連續的(從低位到高位),可是堆中卻存在着暫時不能被分配卻已經無用的對象內存塊

  當一個引用類型對象被初始時,會經過指向堆上可用空間的指針分配一塊連續的內存,而後使堆棧上的引用指向堆上剛剛分配的這塊內存塊。下圖展現了託管堆的內存分配方式:

  如上圖所示,.NET程序經過分配在堆棧中的引用來找到分配在託管堆的對象實例。當堆棧中的引用退出做用域時,這時僅僅就斷開和實際對象實例的引用聯繫。而當託管堆中的內存不夠時,.NET會開始執行GC(垃圾回收)機制。GC是一個很是複雜的過程,它不只涉及託管堆中對象的釋放,並且須要移動合併託管堆中的內存塊。當GC以後,堆中再也不被使用的對象實例纔會被部分釋放(注意並非徹底釋放),而在這以前,它們在堆中是暫時不可用的。在C/C++中,因爲沒有GC,所以能夠直接free/delete來釋放內存。

  (3).NET中的非託管堆

  .NET程序還包含了非託管堆,全部須要分配堆內存的非託管資源將會被分配到非託管堆上。非託管的堆須要程序員用指針手動地分配和釋放內存,.NET中的GC和內存管理不適用於非託管堆,其內存塊也不會被合併移動,因此非託管堆的內存分配是按塊的、不連續的。所以,這也解釋了咱們爲什麼在使用非託管資源(如:文件流、數據庫鏈接等)須要手動地調用Dispose()方法進行內存釋放的緣由。

2.2 執行string abc="aaa"+"bbb"+"ccc"共分配了多少內存?

  這是一個經典的基礎知識題目,它涉及了字符串的類型、堆棧和堆的內存分配機制,所以被不少人拿來考覈開發者的基礎知識功底。首先,咱們都知道,判斷值類型的標準是查看該類型是否會繼承自System.ValueType,經過查看和分析,string直接繼承於System.Object,所以string是引用類型,其內存分配會遵守引用類型的規範,也就是說以下的代碼將會在堆棧上分配一塊存儲引用的內存,而後再在堆上分配一塊存儲字符串實例對象的內存。

    string a = "edc";

  如今再來看看string abc="aaa"+"bbb"+"ccc",按照常規的思路,字符串具備不可變性,大部分人會認爲這裏的表達式會涉及不少臨時變量的生成,可能C#編譯器會先執行"aaa"+"bbb",而且把結果值賦給一個臨時變量,再執行臨時變量和"ccc"相加,最後把相加的結果再賦值給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";這樣的表達式被C#編譯器當作一個完整的字符串"aaabbbccc",而不是執行某些拼接方法,能夠將其看做是C#編譯器的優化,因此在本次內存分配中只是在棧中分配了一個存儲字符串引用的內存塊,以及在託管堆分配了一塊存儲"aaabbbccc"字符串對象的內存塊。

  那麼,咱們的常規思路在.NET程序中又是怎麼體現的呢?咱們來看一下一段代碼:

    int num = 1;
    string str = "aaa" + num.ToString();
    Console.WriteLine(str);

  這裏咱們首先初始化了一個int類型的變量,其次初始化了一個string類型的字符串,並執行 + 操做,這時咱們來看看其對應的IL代碼:

  如上圖所示,在這段代碼中執行 + 操做,會調用String的Concat方法,該方法須要傳入兩個string類型的參數,也就產生了另外一個string類型的臨時變量。換句話說,在這次內存分配中,堆棧中會分配一個存儲字符串引用的內存塊,在託管堆則分配了兩塊內存塊,分別存儲了存儲"aaa"字符串對象和"1"字符串對象。

  可能這段代碼仍是不熟悉,咱們再來看看下面一段代碼,咱們就感受十分親切熟悉了:

    string str = "aaa";
    str += "bbb";
    str += "ccc";
    Console.WriteLine(str);

  其對應的IL代碼以下圖所示:

  如圖能夠看出,在拼接過程當中產生了兩個臨時字符串對象,並調用了兩次String.Concat方法進行拼接,就不用多解釋了。

2.3 簡要說說.NET中GC的運行機制

  GC是垃圾回收(Garbage Collect)的縮寫,它是.NET衆多機制中最爲重要的一部分,也是對咱們的代碼書寫方式影響最大的機制之一。.NET中的垃圾回收是指清理託管堆上不會再被使用的對象內存,而且移動仍在被使用的對象使它們緊靠託管堆的一邊。下圖展現了一次垃圾回收以後託管堆上的變化(這裏僅僅爲了說明,簡化了GC的執行過程,省略了包含Finalize方法對象的處理以及大對象分配的特殊性):

  如上圖所示,咱們能夠知道GC的執行過程分爲兩個基本動做:

  (1)一是找到全部再也不被使用的對象:對象A和對象C,並標記爲垃圾;

  (2)二是移動仍在被使用的對象:對象B和對象D。

  這樣以後,對象A和對象C所佔用的內存空間就被騰空出來,以備下次分配的時候使用。

PS:一般狀況下,咱們不須要手動干預垃圾回收的執行,不過CLR仍然提供了一個手動執行垃圾回收的方法:GC.Collect()。當咱們須要在某一批對象再也不使用而且及時釋放內存的時候能夠調用該方法來實現。But,垃圾回收的運行成本較高(涉及到了對象塊的移動、遍歷找到再也不被使用的對象、不少狀態變量的設置以及Finalize方法的調用等等),對性能影響也較大,所以咱們在編寫程序時,應該避免沒必要要的內存分配,也儘可能減小或避免使用GC.Collect()來執行垃圾回收

2.4 Dispose和Finalize方法在什麼時候被調用?

  因爲有了垃圾回收機制的支持,對象的析構(或釋放)和C++有了很大的不一樣,這就須要咱們在設計類型的時候,充分理解.NET的機制,明確怎樣利用Dispose方法和Finalize方法來保證一個對象正確而高效地被析構。

  (1)Dispose方法

    // 摘要:
    //     定義一種釋放分配的資源的方法。
    [ComVisible(true)]
    public interface IDisposable
    {
        // 摘要:
        //     執行與釋放或重置非託管資源相關的應用程序定義的任務。
        void Dispose();
    }

  Microsoft考慮到不少狀況下程序員仍然但願在對象再也不被使用時進行一些清理工做,因此.NET提供了IDispose接口而且在其中定義了Dispose方法。一般咱們會在Dispose方法中實現一些託管對象和非託管對象的釋放以及業績業務邏輯的結束工做等等。

  But,即便咱們實現了Dispose方法,也不能獲得任何有關釋放的保證,Dispose方法的調用依賴於類型的使用者,當類型被不恰當地使用,Dispose方法將不會被調用。所以,咱們通常會藉助using等語法來幫助Dispose方法被正確調用。

  (2)Finalize方法

  剛剛提到Dispose方法的調用依賴於類型的使用者,爲了彌補這一缺陷,.NET還提供了Finalize方法。Finalize方法相似於C++中的析構函數(方法),但又和C++的析構函數不一樣。Finalize在GC執行垃圾回收時被調用,其具體機制以下:

  ①當每一個包含Finalize方法的類型的實例對象被分配時,.NET會在一張特定的表結構中添加一個引用而且指向這個實例對象,暫且稱該表爲「析構方法的對象表」;

  ②當GC執行而且檢測到一個不被使用的對象時,須要進一步檢查「帶析構方法的對象表」來查詢該對象類型是否含有Finalize方法,若是沒有則將該對象視爲垃圾,若是存在則將該對象的引用移動到另一張表,暫且稱其爲「析構的對象表」,而且該對象實例仍然被視爲在被使用。

  ③CLR將有一個單獨的線程負責處理「待析構的對象表」,其執行方法內部就是依次經過調用其中每一個對象的Finalize方法,而後刪除引用,這時託管堆中的對象實例就被視爲再也不被使用。

  ④下一個GC執行時,將釋放已經被調用Finalize方法的那些對象實例。

  (3)結合使用Dispose和Finalize方法:標準Dispose模式

  Finalize方法因爲有CLR保證調用,所以比Dispose方法更加安全(這裏的安全是相對的,Dispose須要類型使用者的及時調用),但在性能方面Finalize方法卻要差不少。所以,咱們在類型設計時通常都會使用標準Dispose模式:Finalize方法做爲Dispose方法的後備,只有在使用者沒有調用Dispose方法的狀況下,Finalize方法才被視爲須要執行。這一模式保證了對象可以被高效和安全地釋放,已經被普遍使用。

  下面的代碼則是實現這種標準Dispose模式的一個模板:

    public class BaseTemplate : IDisposable
    {
        // 標記對象是否已經被釋放
        private bool isDisposed = false;
        // Finalize方法
        ~BaseTemplate()
        {
            Dispose(false);
        }
        // 實現IDisposable接口的Dispose方法
        public void Dispose()
        {
            Dispose(true);
            // 告訴GC此對象的Finalize方法再也不須要被調用
            GC.SuppressFinalize(this);
        }
        // 虛方法的Dispose方法作實際的析構工做
        protected virtual void Dispose(bool isDisposing)
        {
            // 當對象已經被析構,則沒必要再繼續執行
            if(isDisposed)
            {
                return;
            }

            if(isDisposing)
            {
                // Step1:在這裏釋放託管資源
            }

            // Step2:在這裏釋放非託管資源

            // Step3:最後標記對象已被釋放
            isDisposed = true;
        }

        public void MethodA()
        {
            if(isDisposed)
            {
                throw new ObjectDisposedException("對象已經釋放");
            }

            // Put the logic code of MethodA
        }

        public void MethodB()
        {
            if (isDisposed)
            {
                throw new ObjectDisposedException("對象已經釋放");
            }

            // Put the logic code of MethodB
        }
    }

    public sealed class SubTemplate : BaseTemplate
    {
        // 標記子類對象是否已經被釋放
        private bool disposed = false;

        protected override void Dispose(bool isDisposing)
        {
            // 驗證是否已被釋放,確保只被釋放一次
            if(disposed)
            {
                return;
            }

            if(isDisposing)
            {
                // Step1:在這裏釋放託管的而且在這個子類型中申明的資源
            }

            // Step2:在這裏釋放非託管的而且這個子類型中申明的資源

            // Step3:調用父類的Dispose方法來釋放父類中的資源
            base.Dispose(isDisposing);
            // Step4:設置子類的釋放標識
            disposed = true;
        }
    }
View Code

  真正作釋放工做的只是受保護的虛方法Dispose,它接收一個bool參數,主要用於區分調用者是類型的使用者仍是.NET的GC機制。二者的區別在於經過Finalize方法釋放資源時不能再釋放或使用對象中的託管資源,這是由於這時的對象已經處於不被使用的狀態,頗有可能其中的託管資源已經被釋放掉了。在Dispose方法中GC.SuppressFinalize(this)告訴GC此對象在被回收時不須要調用Finalize方法,這一句是改善性能的關鍵,記住實現Dispose方法的本質目的就在於避免全部釋放工做在Finalize方法中進行

2.5 GC中代(Generation)是什麼,分爲幾代?

  在.NET的GC執行垃圾回收時,並非每次都掃描託管堆內的全部對象實例,這樣作太耗費時間並且沒有必要。相反,GC會把全部託管堆內的對象按照其已經再也不被使用的可能性分爲三類,而且從最有可能不被使用的類別開始掃描,.NET對這樣的分類類別有一個稱呼:代(Generation)。

  GC會把全部的託管堆內的對象分爲0代、1代和2代:

  第0代,新近分配在堆上的對象,歷來沒有被垃圾收集過。任何一個新對象,當它第一次被分配在託管堆上時,就是第0代。  

  第1代,經歷過一次垃圾回收後,依然保留在堆上的對象。  

  第2代,經歷過兩次或以上垃圾回收後,依然保留在堆上的對象。若是第2代對象在進行完垃圾回收後空間仍然不夠用,則會拋出OutOfMemoryException異常

  對於這三代,咱們須要知道的是並非每次垃圾回收都會同時回收3個代的全部對象,越小的代擁有着越多被釋放的機會

  CLR對於代的基本算法是:每執行N次0代的回收,纔會執行一次1代的回收,而每執行N次1代的回收,纔會執行一次2代的回收。當某個對象實例在GC執行時被發現仍然在被使用,它將被移動到下一個代中上,下圖簡單展現了GC對三個代的回收操做。

  根據.NET的垃圾回收機制,0代、1代和2代的初始分配空間分別爲256KB、2M和10M。說完分代的垃圾回收設計,也許咱們會有疑問,爲何要這樣弄?其實分代並非空穴來風的設計,而是參考了這樣一個事實:

一個對象實例存活的時間越長,那麼它就具備更大的機率去存活更長的時間。換句話說,最有可能立刻就不被使用的對象實例,每每是那些剛剛被分配的對象實例,並且新分配的對象實例一般都會被立刻大量地使用。這也解釋了爲何0代對象擁有最多被釋放的機會,而且.NET也只爲0代分配了一塊只有256KB的小塊邏輯內存,以使得0代對象有機會被所有放入處理器的緩存中去,這樣作的結果就是使用頻率最高而且最有可能立刻能夠被釋放的對象實例擁有了最高的使用效率和最快的釋放速度。

  由於一次GC回收以後仍然被使用的對象會被移動到更高的代上,所以咱們須要避免保留已經再也不被使用的對象引用將對象的引用置爲null是告訴.NET該對象不須要再使用的最直接的方法。

  在前面咱們提到Finalize方法會大幅影響性能,經過結合對代的理解,咱們能夠知道:在帶有Finalize方法的對象被回收時,該對象會被視爲正在被使用從而被留在託管堆中,且至少要等一個GC循環才能被釋放(爲何是至少一個?由於這取決於執行Finalize方法的線程的執行速度)。很明顯,須要執行Finalize方法的那些對象實例,被真正釋放時最樂觀的狀況下也已經位於1代的位置上了,而若是它們是在1代上纔開始釋放或者執行Finalize方法的線程運行得慢了一點,那該對象就在第2代上才被釋放,相對於0代,這樣的對象實例在堆中存留的時間將長不少。

2.6 GC機制中如何判斷一個對象仍然在被使用?

  在.NET中引用類型對象實例一般經過引用來訪問,而GC判斷堆中的對象是否仍然在被使用的依據也是引用。簡單地說:當沒有任何引用指向堆中的某個對象實例時,這個對象就被視爲再也不使用

  在GC執行垃圾回收時,會把引用分爲如下兩類:

  (1)根引用:每每指那些靜態字段的引用,或者存活的局部變量的引用;

  (2)非根引用:指那些不屬於根引用的引用,每每是對象實例中的字段。

  垃圾回收時,GC從全部仍在被使用的根引用出發遍歷全部的對象實例,那些不能被遍歷到的對象將被視爲再也不被使用而進行回收。咱們能夠經過下面的一段代碼來直觀地理解根引用和非根引用:

    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.ReadKey();
            Console.WriteLine(a);
        }
    }

    public class Employee
    {
        public Employee boss;

        public override string ToString()
        {
            if(boss == null)
            {
                return "No boss";
            }

            return "One boss";
        }
    }
View Code

  上述代碼中一共有兩個局部變量和一個靜態變量,這些引用都是根引用。而其中一個局部變量 a 擁有一個成員實例對象,這個引用就是一個非跟引用。下圖展現了代碼執行到Console.ReadKey()這行代碼時運行垃圾回收時的狀況。

  從上圖中能夠看出,在執行到Console.ReadKey()時,存活的根引用有staticEmployee和a,前者由於它是一個公共靜態變量,然後者則由於後續代碼還會使用到a。經過這兩個存活的根引用,GC會找到一個非跟引用staticEmployee.boss,而且發現三個仍然存活的對象。而b的對象則將被視爲再也不使用從而被釋放。(更簡單地確保b對象再也不被視爲在被使用的方法時把b的引用置爲null,即b=null;)

  此外,當一個從根引用觸發的遍歷抵達一個已經被視爲在使用的對象時,將結束這一個分支的遍歷,這樣作能夠避免陷入死循環。

2.7 .NET中的託管堆中是否可能出現內存泄露的現象?

  首先,必須明確一點:即便在擁有垃圾回收機制的.NET託管堆上,仍然是有可能發生內存泄露現象的

  其次,什麼是內存泄露?內存泄露是指內存空間上產生了再也不被實際使用卻又不能被分配的內存空間,其意義很普遍,像內存碎片、不完全的對象釋放等都屬於內存泄露現象。內存泄露將致使主機的內存隨着程序的運行而逐漸減小,不管其表現形式怎樣,它的危害是很大的,所以咱們須要努力地避免。

  按照內存泄露的定義,咱們能夠知道在大部分的時候.NET中的託管堆中存在着短暫的內存泄露狀況,由於對象一旦再也不被使用,須要等到下一個GC時纔會被釋放。這裏列舉幾個在.NET中常見的幾種對系統危害較大的內存泄露狀況,咱們在實際開發中須要極力避免:

  (1)大對象的分配

  .NET中全部的大對象(這裏主要是指對象的大小超過指定數值[85000字節])將分配在託管堆內一個特殊的區域內,暫且將其稱爲「大對象堆」(這也算是CLR對於GC的一個優化策略)。大對象堆中最重要的一個特色就是:沒有代級的概念,全部對象都被視爲第2代回收大對象堆內的對象時,其餘的大對象不會被移動,這是考慮到大規模地移動對象須要耗費過多的資源。這樣,在程序過多地分配和釋放大對象以後,就會產生不少內存碎片。下圖解釋了這一過程:

  如圖所示能夠看出,隨着對象的分配和釋放不斷進行,在不進行對象移動的大對象堆內,將不可避免地產生小的內存碎片。咱們所須要作的就是儘可能減小大對象的分配次數,尤爲是那些做爲局部變量的,將被大規模分配和釋放的大對象,典型的例子就是String類型。

  (2)不恰當地保存根引用

  最簡單的一個錯誤例子就是不恰當地把一個對象申明爲公共靜態變量,一個公共的靜態變量將一直被GC視爲一個在使用的根引用。更糟糕的是:當這個對象內部還包含更多的對象引用時,這些對象一樣不會被釋放。例以下面一段代碼:

    public class Program
    {
        // 公共靜態大對象
        public static RefRoot bigObject = new RefRoot("test");

        public static void Main(string[] args)
        {
            
            Console.ReadKey();
        }
    }

    public class RefRoot
    {
        // 這是一個佔用大量內存的成員
        public string[] BigMember;

        public RefRoot(string content)
        {
            // 初始化大對象
            BigMember = new string[1000];
            for (int i = 0; i < 1000; i++)
            {
                BigMember[i] = content;
            }
        }
    }
View Code

  在代碼中,定義了一個公共靜態的大對象,這個對象將直到程序運行結束後纔會被GC釋放掉。若是在整個程序中各個類型不斷地使用這個靜態成員,那這樣的設計有助於減小大對象堆內的內存碎片,可是若是整個程序極少地甚至只有一次使用了這個成員,那考慮到它佔用的內存會影響總體系統性能,設計時則應該考慮設計成實例變量,以便GC可以及時釋放它。

  (3)不正確的Finalize方法

  前面已經介紹了Finalize方法時由GC的一個專用的線程進行調用,拋開Microsoft怎樣實現的這個具體的調度算法,有一點能夠確定的是:不正確的Finalize方法將致使Finalize方法不能被正確執行。若是系統中全部的Finalize方法不能被正確執行,包含它們的對象也只能駐留在託管堆內不能被釋放,這樣的狀況將會致使嚴重的後果。

  那麼,什麼是不正確的Finalize方法?Finalize方法應該只致力於快速而簡單地釋放非託管資源,而且儘量快地返回。相反,不正確的Finalize方法則可能包含如下這樣的一些代碼:

  ①沒有保護地寫文件日誌;

  ②訪問數據庫;

  ③訪問網絡;

  ④把當前對象賦給某個存活的引用;

  例如,當Finalize方法試圖訪問文件系統、數據庫或者網絡時,將會有資源爭用和等待的潛在危險。試想一個不斷嘗試訪問離線數據庫的Finalize方法,將會在長時間內不會返回,這不只影響了對象的釋放,也使得排在Finalize方法隊列中的全部後續對象得不到釋放,這個連鎖反應將會致使很快地形成內存耗盡。此外,若是在Finalize方法中把對象自身又賦給了另一個存活的引用,這時對象內的一部分資源已經被釋放掉了,而另一部分尚未,當這樣一個對象被激活後,將致使不可預知的後果。

參考資料

(1)朱毅,《進入IT企業必讀的200個.NET面試題》

(2)張子陽,《.NET之美:.NET關鍵技術深刻解析》

(3)王濤,《你必須知道的.NET》

 

相關文章
相關標籤/搜索