2017年9月2日 星期六

.NET C# 使用類別建構式與解構式,來觀察 GC 記憶體回收運作機制

當我們在 .NET 環境下進行程式開發,並且想要建立一個類別 Class 的物件 Object(或稱為執行個體 Instance)的時候,可以使用 new 運算子,並且緊接著該類別的建構式函式,就可以取得該物件;當然,這個時候我們會使用物件變數來取得並且儲存參考到該物件位置。
可是,當我們不再需要使用到該物件的時候,只要該物件沒有被任何屬性、物件變數參考到,這個時候,記憶體回收機制 Garbage Collection 就會自動地把這些沒有人認領的記憶體進行回收,開發人員是不需要做任何額外的處理與透過任何程式碼的撰寫,就可以讓您的應用程式,可以繼續使用這些已經回收的記憶體空間。
在這個筆記之中,我們將會透過一個類別 CountObject 類別,記錄下這個類別總共生成了多少個物件;在這裡,我們使用了底下的技巧:
  • 靜態的屬性
    當物件被生成的時候,會先執行該類別的建構式,因此,一旦建構式被呼叫的時候,我們在建構式內,會自動地將這個靜態屬性值加一,這樣,所們可以隨時查詢這個類別的靜態屬性,就可以知道透過該類別所產生的物件,總共有多少數量存在於記憶體中。
  • 解構式函式
    當該物件所占用的記憶體要被 .NET 系統回收,也就是該物件所占用的記憶體,將可以被其他 .NET 的物件來使用,此時,這個物件所屬的解構式函式將會被呼叫。
    我們將會透過此機運作機制特性,一旦解構式被呼叫的話,我們會在解構式中,將該靜態屬性值減一,代表該類別所生成且存在於記憶體中的物件,將會少一個。
    我們在這個物件上宣告一個欄位 ObjectName,用來記錄下這個新產生的物件名稱;另外,當物件被建立的時候,會在建構式中輸出一段訊息,說明是哪個物件被建立起來了;而當該物件要被記憶體回收的時候,會在解構式中輸出一段訊息,說明是哪個物件要被記憶體回收機制給回收了。
底下是這次所使用到的測試程式。
    public class CountObject
    {
        /// <summary>
        /// 記錄下現在記憶體中,總共存在這多少個物件
        /// </summary>
        private static int _TotalObjects;

        public static int TotalObjects
        {
            get { return _TotalObjects; }
            set { _TotalObjects = value; }
        }

        string ObjectName;

        public CountObject()
        {
            // 若有該類別的物件產生,則計數器會加一
            CountObject.TotalObjects += 1;
            Console.WriteLine($"現在總共有 {CountObject.TotalObjects} 物件");
        }

        public CountObject(string objectName) : this()
        {
            Console.WriteLine($"有新的物件要產生 {objectName}");
            ObjectName = objectName;
        }

        ~CountObject()
        {
            Console.WriteLine($"有物件要回收 {ObjectName}");
            // 若有物件被回收,則該計數器會減一
            CountObject.TotalObjects -= 1;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("現在要產生六個物件");
            new CountObject("物件1");
            new CountObject("物件2");
            new CountObject("物件3");
            new CountObject("物件4");
            var fooObject = new CountObject("物件5");
            var fooTempObject = new CountObject("物件6");
            Console.WriteLine($"Total Objects is {CountObject.TotalObjects}");
            Console.WriteLine("Press any key for continuing...");
            Console.ReadKey();

            // 進行記憶體回收工作
            Console.WriteLine("進行記憶體回收工作");
            GC.Collect(2, GCCollectionMode.Forced);

            // 將要休息三秒鐘,讓背景記憶體回收程序,可以有足夠的時間,進行記憶體回收
            Console.WriteLine("將要休息三秒鐘,讓背景記憶體回收程序,可以有足夠的時間,進行記憶體回收");
            Thread.Sleep(3000);
            Console.WriteLine($"Total Objects is {CountObject.TotalObjects}");
            Console.WriteLine("Press any key for continuing...");
            Console.ReadKey();

            Console.WriteLine("將其中一個物件變數 fooTempObject,設為空值 null,因此,該變數 fooTempObject 所指向的物件,將會被記憶體回收");
            fooTempObject = null;
            // 進行記憶體回收工作
            Console.WriteLine("進行記憶體回收工作");
            GC.Collect(2, GCCollectionMode.Forced);

            // 將要休息三秒鐘,讓背景記憶體回收程序,可以有足夠的時間,進行記憶體回收
            Console.WriteLine("將要休息三秒鐘,讓背景記憶體回收程序,可以有足夠的時間,進行記憶體回收");
            Thread.Sleep(3000);
            Console.WriteLine($"Total Objects is {CountObject.TotalObjects}");
            Console.WriteLine("Press any key for continuing...");
            Console.ReadKey();
        }
    }
底下是這段測試程式碼執行的輸出結果
現在要產生六個物件
現在總共有 1 物件
有新的物件要產生 物件1
現在總共有 2 物件
有新的物件要產生 物件2
現在總共有 3 物件
有新的物件要產生 物件3
現在總共有 4 物件
有新的物件要產生 物件4
現在總共有 5 物件
有新的物件要產生 物件5
現在總共有 6 物件
有新的物件要產生 物件6
Total Objects is 6
Press any key for continuing...
 進行記憶體回收工作
將要休息三秒鐘,讓背景記憶體回收程序,可以有足夠的時間,進行記憶體回收
有物件要回收 物件4
有物件要回收 物件3
有物件要回收 物件2
有物件要回收 物件1
Total Objects is 2
Press any key for continuing...
 將其中一個物件變數 fooTempObject,設為空值 null,因此,該變數 fooTempObject 所指向的物件,將會被記憶體回收
進行記憶體回收工作
將要休息三秒鐘,讓背景記憶體回收程序,可以有足夠的時間,進行記憶體回收
有物件要回收 物件6
Total Objects is 1
Press any key for continuing...

測試步驟說明

首先,我們使用底下程式碼產生了六個物件,我們看到前面四個敘述,是有產生四個物件,可是,並沒有任何物件變數會參考到這四個物件,因此,這四個物件也就是沒有人使用到的物件,一旦進行了記憶體回收的動作,這四個物件,將會被記憶體回收機制回收掉。
而後面兩個敘述,則是分別有兩個物件變數指向他們,因此,將不會被記憶體回收機制做回收。
不管如何,當底下程式碼執行完成之後,我們查詢到 CountObject 這個類別總共有六個物件存在於記憶體中(雖然有四個物件是沒有被任何程式碼參考到)。
            new CountObject("物件1");
            new CountObject("物件2");
            new CountObject("物件3");
            new CountObject("物件4");
            var fooObject = new CountObject("物件5");
            var fooTempObject = new CountObject("物件6");
接著,我們呼叫了 GC.Collect,強制記憶體回收機制進行記憶體回收(也就是把沒有在使用到的物件,從記憶體中釋放出來,該空間就可以提供給其他物件來使用。通常來說,我們在撰寫 .NET 程式碼的時候,非常強烈的,不建議自行呼叫記憶體回收機制來執行記憶體回收的動作,因為,.NET系統會自動決定何時用來執行記憶體回收的行為,並且,也會動態的依據當時系統運作形況,自行調整記憶體回收機制的運作行為。
GC.Collect(2, GCCollectionMode.Forced);
由於我們是要進行測試,所以,我們使用上述的方法,強制運行記憶體回收的機制,讓我們可以儘快地看到記憶體回收的結果。
不過,由於當 .NET 的記憶體回收機制發現到要回收的物件所占用的記憶體,會透過背景執行緒(Client / Server 的 .NET 程式採用不同的方式來進行回收動作),所以,我們就讓我們當前的執行緒暫停個3秒鐘,讓記憶體回收機制可以確實地把沒有被使用到的物件,進行回收;所以,我們使用了底下的程式碼:
Thread.Sleep(3000);
因此,你會看到了,當第一次記憶體回收機制執行的時候,總共有兩個物件記憶體存在 .NET 系統內。
不過,當我們想要把某個物件透過記憶體回收機制,讓他回收掉,這個時候,您將會有兩種選擇,第一個就是不去理會它,讓您的程式依照您的流程來運行,這樣當有不再繼續使用到的物件發生的時候,將會被記憶體回收掉。
第二個,就是您可以透過底下的敘述,就是把指向該物件記憶體位置的物件變數,設定成為 null,這樣,下一次記憶體回收機制開始運作的時候,該物件就會被記憶體回收機制回收掉。
fooTempObject = null;

沒有留言:

張貼留言