Net 高階偵錯之十二:垃圾回收機制以及終端子佇列、物件固定

2023-12-08 15:00:40
一、簡介
    今天是《Net 高階偵錯》的第十二篇文章,這篇文章寫作時間的跨度有點長。這篇文章我們主要介紹 GC 的垃圾回收演演算法,什麼是根物件,根物件的存在區域,我們也瞭解具有解構函式的物件是如何被回收的,終端子佇列和終端子執行緒也做到了眼見為實,最後還介紹了一下大物件堆的回收策略,東西不少,慢慢體會吧。我們瞭解了物件出生、成長、終結的整個生命週期,明白了託管堆的分類、物件的分類、GC 的回收策略,對託管物件和非託管物件都有了跟深入的認識,這些是 Net 框架的底層,瞭解更深,對於我們偵錯更有利。當然了,第一次看視訊或者看書,是很迷糊的,不知道如何操作,還是那句老話,一遍不行,那就再來一遍,還不行,那就再來一遍,俗話說的好,書讀千遍,其意自現。
     如果在沒有說明的情況下,所有程式碼的測試環境都是 Net Framewok 4.8,但是,有時候為了檢視原始碼,可能需要使用 Net Core 的專案,我會在專案章節裡進行說明。好了,廢話不多說,開始我們今天的偵錯工作。

       偵錯環境我需要進行說明,以防大家不清楚,具體情況我已經羅列出來。
          作業系統:Windows Professional 10
          偵錯工具:Windbg Preview(可以去Microsoft Store 去下載)
          開發工具:Visual Studio 2022
          Net 版本:Net Framework 4.8
          CoreCLR原始碼:原始碼下載

二、基礎知識
    1、垃圾回收演演算法
        1.1、簡介
            CLR的垃圾回收採用的是【代回收演演算法】,從宏觀看:來了一個記憶體分配的請求,如果 0 代滿了就會觸發 0 代 GC ,當 1 代滿了就會觸發 1 代 GC,當 2 代滿了就會觸發 2 代 GC。
            整體架構如圖:
            

 


    2、根物件
        2.1、簡介
            C# 的參照跟蹤回收演演算法,核心在於尋找【根物件】,凡是託管堆上的某個物件被【根物件】所參照,GC就不會回收這個物件的。

        2.2、哪裡有根物件
            通常3個地方有根物件。
            a、執行緒棧
                方法作用域下的參照型別,自然就是根物件。
            b、終端子佇列
                帶有解構函式的物件自然會被加入到【終端子佇列】中,終結執行緒會在物件成為垃圾物件後的某個時刻執行物件的解構函式。
            c、控制程式碼表
                凡是被 Strong、Pinned 標記的物件都會被放入到【控制程式碼表】中,比如:static 物件。控制程式碼表就是在 CLR 私有堆中具有一個字典型別的資料結構,用於儲存被 Strong、Pinned 標記的物件。

    3、終端子佇列和終端子執行緒
        3.1、如何檢視終端子佇列
            凡是帶有【解構函式】的物件都會被放入到【終端子佇列】中,我們可以通過 Windbg 使用【!fq】命令檢視。

        3.2、如何觀察終端子執行緒。
            在 C# 程式中,一般 ID=2 的執行緒就是終端子執行緒。它的目的就是用來釋放【終端子佇列】中已經被 GC 處理過的無根物件。

    4、大物件堆
        LOH堆也就是大物件堆,既沒有代的機制,也沒有壓縮的機制,只有「標記清除」,即:GC 觸發時,只會將一個物件標記成 Free 物件。這種 Free 可供後續分配的物件,可以說,以後有新物件產生,會首先存放在 Free 塊中。

三、偵錯過程
    廢話不多說,這一節是具體的偵錯過程,又可以說是眼見為實的過程,在開始之前,我還是要囉嗦兩句,這一節分為兩個部分,第一部分是測試的原始碼部分,沒有程式碼,當然就談不上測試了,偵錯必須有載體。第二部分就是根據具體的程式碼來證實我們學到的知識,是具體的眼見為實。

    1、偵錯原始碼
        1.1、Example_12_1_1
 1 namespace Example_12_1_1
 2 {
 3     internal class Program
 4     {
 5         static void Main(string[] args)
 6         {
 7             Console.WriteLine("請輸入任一字串。。。");
 8             var str=Console.ReadLine();
 9 
10             Console.WriteLine("請觀察 str 是否在 0 代!");
11             Debugger.Break();
12 
13             GC.Collect();
14             Console.WriteLine("請觀察 str 是否在 1 代!");
15             Debugger.Break();
16 
17             GC.Collect();
18             Console.WriteLine("請觀察 str 是否在 2 代!");
19             Debugger.Break();
20         }
21     }
22 }
View Code

        1.2、Example_12_1_2
            Program 類原始碼:
 1 namespace Example_12_1_2
 2 {
 3     internal class Program
 4     {
 5         public static Person3 person3 = new Person3();
 6         static void Main(string[] args)
 7         {
 8             var person1 = new Person1();
 9 
10             FinalizeTest();
11 
12             Console.WriteLine("分配完畢!");
13 
14             Console.ReadLine();
15         }
16 
17         private static void FinalizeTest()
18         {
19             var person2 = new Person2();
20         }
21     }
22 }
View Code

            Person1 類原始碼:

1 internal class Person1
2 {
3 }
View Code

            Person2 類原始碼:

1 internal class Person2
2 {
3     ~Person2()
4     {
5         Console.WriteLine("我是解構函式");
6     }
7 }
View Code

            Person3 類原始碼:

1 internal class Person3
2 {
3 }
View Code

        1.3、Example_12_1_3
            Program 類原始碼:
 1 namespace Example_12_1_3
 2 {
 3     internal class Program
 4     {
 5         static void Main(string[] args)
 6         {
 7             TestFinalize();
 8 
 9             Console.WriteLine("開始觸發GC了!");
10             GC.Collect();
11 
12             Console.ReadLine();
13         }
14 
15         private static void TestFinalize()
16         {
17             var person = new Person();
18         }
19     }
20 }
View Code

            Person 類原始碼:

1 internal class Person
2 {
3     ~Person()
4     {
5         Console.WriteLine("我是解構函式");
6 
7         Console.ReadLine();
8     }
9 }
View Code

        1.4、Example_12_1_4
 1 namespace Example_12_1_4
 2 {
 3     internal class Program
 4     {
 5         static void Main(string[] args)
 6         {
 7             Test();
 8 
 9             Console.WriteLine("1、物件已經分配,請檢視託管堆!");
10             Debugger.Break();
11             GC.Collect();
12 
13             Console.WriteLine("2、GC 已經觸發,請檢視託管堆中的 byte2");
14             Debugger.Break();
15 
16             Console.WriteLine("3、已分配 byte4,檢視是否 Free 塊中");
17             var byte4 = new byte[280000];
18             Debugger.Break();
19         }
20 
21         public static byte[] byte1;
22         public static byte[] byte3;
23 
24         private static void Test()
25         {
26             byte1 = new byte[185000];
27             var byte2 = new byte[285000];
28             byte3 = new byte[385000];
29         }
30     }
31 }
View Code

    2、眼見為實
        專案的所有操作都是一樣的,所以就在這裡說明一下,但是每個測試例子,都需要重新啟動,並載入相應的應用程式,載入方法都是一樣的。流程如下:我們編譯專案,開啟 Windbg,點選【檔案】----》【launch executable】附加程式,開啟偵錯程式的介面,程式已經處於中斷狀態。我們需要使用【g】命令,繼續執行程式,然後到達指定地點停止後,我們可以點選【break】按鈕,就可以偵錯程式了。有時候可能需要切換到主執行緒,可以使用【~0s】命令。

        2.1、我們可以通過誘導GC的方式觀察一個物件如何從 0 代到 2 代的提升過程的。
            偵錯原始碼:Example_12_1_1
            程式輸出:請輸入任一字串。。。,然後,我們輸入一個很長的 a 的字串,我的值是:aaaaaaaaaaaaaaaaaaaa。【var myvalue=Console.ReadLine()】由這條程式碼我們知道,myvalue是 Main() 方法的區域性變數,所以我們可以使用【!clrstack -l】命令,檢視當前的執行緒棧,就可以知道這個字串。
 1 0:000> !clrstack -l
 2 OS Thread Id: 0x323c (0)
 3 Child SP       IP Call Site
 4 012fec70 766ef262 [HelperMethodFrame: 012fec70] System.Diagnostics.Debugger.BreakInternal()
 5 012fecec 6e45f195 System.Diagnostics.Debugger.Break() [f:\dd\ndp\clr\src\BCL\system\diagnostics\debugger.cs @ 91]
 6 
 7 012fed14 031a087d Example_12_1_1.Program.Main(System.String[]) [E:\Visual Studio 2022...\Example_12_1_1\Program.cs @ 16]
 8     LOCALS:
 9         <CLR reg> = 0x033c4e80 我們的區域性變數。
10 
11 012fee88 7033f036 [GCFrame: 012fee88] 

            我們看到,紅色標記的就是區域性變數。我們看看它的內容,使用【!dumpobj /d 0x033c4e80 】。

 1 0:000> !dumpobj /d 0x033c4e80
 2 Name:        System.String
 3 MethodTable: 6d8e24e4
 4 EEClass:     6d9e7690
 5 Size:        54(0x36) bytes
 6 File:        C:\Windows\Microsoft.Net\assembly\GAC_32\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
 7 String:      aaaaaaaaaaaaaaaaaaaa 我們輸入的值。
 8 Fields:
 9       MT    Field   Offset                 Type VT     Attr    Value Name
10 6d8e42a8  4000283        4         System.Int32  1 instance       20 m_stringLength
11 6d8e2c9c  4000284        8          System.Char  1 instance       61 m_firstChar
12 6d8e24e4  4000288       70        System.String  0   shared   static Empty
13     >> Domain:Value  014e2318:NotInit  <<

            我們的程式輸出:請觀察 aaaaaaaaaaaaaaaaaaaa 是否在 0 代!我們使用【!gcwhere 0x033c4e80】命令就可以看到這個字串在堆上的情況。

1 0:000> !gcwhere 0x033c4e80
2 Address   Gen   Heap   segment            begin              allocated           size
3 033c4e80   0      0     033c0000   033c1000   033c5ff4    0x38(56)

            當前字串還沒有執行垃圾回收,所以在 0 代。我們繼續【g】,程式輸出:請觀察 aaaaaaaaaaaaaaaaaaaa 是否在 1 代!我們繼續使用【!gcwhere 0x033c4e80】命令檢視具體的情況。

1 0:000> !gcwhere 0x033c4e80
2 Address   Gen   Heap   segment            begin              allocated           size
3 033c4e80   1      0     033c0000   033c1000   033c7150    0x38(56)

            我們的字串已經在 1 代了。我們繼續【g】,程式輸出:請觀察 aaaaaaaaaaaaaaaaaaaa 是否在 2 代!!我們繼續使用【!gcwhere 0x033c4e80】命令檢視具體的情況。

1 0:000> !gcwhere 0x033c4e80
2 Address    Gen   Heap   segment            begin              allocated           size
3 033c4e80   2      0     033c0000   033c1000   033c715c    0x38(56)
            GC回收有兩種方式,一種是壓縮回收,一種是標記回收,在這裡字串的地址沒有變,主要是認為字串沒有必要執行壓縮,只是代的劃分變了,所以帶的劃分不過是一個邏輯值,這個值是可以改變的,所以執行標記回收。

        2.2、我們檢視執行緒棧上的根物件。
            偵錯原始碼:Example_12_1_2
            當我們進入調成介面後,【g】繼續執行,程式輸出:分配完畢!我們點選【break】按鈕進入中斷模式,由於我們需要檢視 Main() 方法的執行緒棧,必須切換到主執行緒,執行【~0s】命令就可以,我們開始進入偵錯環節了。
 1 0:000> !clrstack -a
 2                 OS Thread Id: 0x31a4 (0)
 3                 Child SP       IP Call Site
 4                 00b8f2dc 77e710fc [InlinedCallFrame: 00b8f2dc] 
 5                 ......
 6                 00b8f3d8 02a50929 Example_12_1_2.Program.Main(System.String[]) [E:\Visual Studio 2022\\Example_12_1_2\Program.cs @ 16]
 7                     PARAMETERS:
 8                         args (0x00b8f3e4) = 0x02c924c8
 9                     LOCALS:
10                         0x00b8f3e0 = 0x02c924f8
11 
12                 00b8f560 7158f036 [GCFrame: 00b8f560] 

            0x02c924f8 就是 Person1物件的地址,我們可以使用【!DumpObj /d 02c924f8】命令檢視 Person1的詳情。

1                 0:000> !DumpObj /d 02c924f8
2                 Name:        Example_12_1_2.Person1
3                 MethodTable: 010d4e80
4                 EEClass:     010d13e4
5                 Size:        12(0xc) bytes
6                 File:        E:\Visual Studio 2022\Example_12_1_2\bin\Debug\Example_12_1_2.exe
7                 Fields:
8                 None

            的確是 Person1 物件,我們繼續使用【!gcroot 02c924f8】命令檢視在哪裡被參照了。

1                 0:000> !gcroot 02c924f8
2                 Thread 31a4:
3                     00b8f3d8 02a50929 Example_12_1_2.Program.Main(System.String[]) [E:\Visual Studio 2022\Example_12_1_2\Program.cs @ 16]
4                         ebp+8: 00b8f3e0(這個是棧地址,和 !clrstack -a 結果中的  LOCALS: 0x00b8f3e0(棧地址) = 0x02c924f8(物件地址))
5                             ->  02c924f8 Example_12_1_2.Person1
6 
7                 Found 1 unique roots (run '!GCRoot -all' to see all roots).

        2.3、我們檢視終端子佇列上的根物件。
            偵錯原始碼:Example_12_1_2
            當我們進入調成介面後,【g】繼續執行,程式輸出:分配完畢!我們點選【break】按鈕進入中斷模式,由於我們需要檢視 Main() 方法的執行緒棧,必須切換到主執行緒,執行【~0s】命令就可以,我們開始進入偵錯環節了。我們可以在同一個專案程式碼:Example_12_1_2 中偵錯處理,是否退出,在重新執行 Windbg可自行決定
            我們現在託管堆中查詢一下 Person 2物件,可以執行【!dumpheap -type Person2】命令,就可以找到 Person2 物件的地址。
1 0:000> !dumpheap -type Person2
2  Address       MT     Size
3 029c2508 00884e8c       12     
4 
5 Statistics:
6       MT    Count    TotalSize Class Name
7 00884e8c        1           12 Example_12_1_2.Person2

            我們找到了 Person2 物件的地址:029c2508,我們可以檢視是否是 Person2。

1 0:000> !DumpObj /d 029c2508
2 Name:        Example_12_1_2.Person2
3 MethodTable: 00884e8c
4 EEClass:     008813a8
5 Size:        12(0xc) bytes
6 File:        E:\Visual Studio 2022\Example_12_1_2\bin\Release\Example_12_1_2.exe
7 Fields:
8 None

            我們有了 Person2 物件地址,我們可以執行【!gcroot】命令,看看還有誰參照。

1 0:000> !gcroot 029c2508
2 Finalizer Queue(終端子佇列):
3     029c2508
4     -> 029c2508 Example_12_1_2.Person2
5 
6 Warning: These roots are from finalizable objects that are not yet ready for finalization.
7 This is to handle the case where objects re-register themselves for finalization.
8 These roots may be false positives.
9 Found 1 unique roots (run '!GCRoot -all' to see all roots).

        2.4、我們再看看控制程式碼表所儲存的根物件。
            偵錯原始碼:Example_12_1_2
            當我們進入調成介面後,【g】繼續執行,程式輸出:分配完畢!我們點選【break】按鈕進入中斷模式,我們開始進入偵錯環節了。我們可以在同一個專案程式碼:Example_12_1_2 中偵錯處理,是否退出,在重新執行 Windbg可自行決定
            我們這一個環節主要是通過 Person3 物件來證明的,我們首先查詢 Person3 物件。
1                 0:000> !dumpheap -type Person3
2                  Address       MT     Size
3                 02c924d4 010d4e24       12     
4 
5                 Statistics:
6                       MT    Count    TotalSize Class Name
7                 010d4e24        1           12 Example_12_1_2.Person3
8                 Total 1 objects

            紅色標記的就是 Person3 物件的地址,我們直接使用【!gcroot 02c924d4】命令看一看。

1                 0:000> !gcroot 02c924d4
2                 HandleTable:
3                     010b13ec (pinned handle)(pinned)
4                     -> 03c93568 System.Object[](控制程式碼表地址)
5                     -> 02c924d4 Example_12_1_2.Person3
6 
7                 Found 1 unique roots (run '!GCRoot -all' to see all roots).

            如果想檢視控制程式碼表的詳情,可以執行如下命令。

1                 0:000> !da -details 03c93568
2                 Name:        System.Object[]
3                 MethodTable: 700b2788
4                 EEClass:     701b7820
5                 Size:        8172(0x1fec) bytes
6                 Array:       Rank 1, Number of elements 2040, Type CLASS
7                 Element Methodtable: 700b2734
8                 ......

        2.5、如何檢視終端子佇列。
            偵錯原始碼:Example_12_1_2
            這個命令使用很簡單,當我們進入調成介面後,【g】繼續執行,程式輸出:分配完畢!我們點選【break】按鈕進入中斷模式,我們直接輸入【!fq】命令就可以了。
 1 0:000> !fq
 2 SyncBlocks to be cleaned up: 0
 3 Free-Threaded Interfaces to be released: 0
 4 MTA Interfaces to be released: 0
 5 STA Interfaces to be released: 0
 6 ----------------------------------
 7 generation 0 has 8 finalizable objects (01585990->015859b0)【0代有8個可以被回收的物件。】
 8 generation 1 has 0 finalizable objects (01585990->01585990)【1代有0個可以被回收的物件。】
 9 generation 2 has 0 finalizable objects (01585990->01585990)【2代有0個可以被回收的物件。】
10 Ready for finalization 0 objects (015859b0->015859b0),【015859b0->015859b0】這個區間會被終端子執行緒讀取的,就可以釋放這個區間的資源。
11 Statistics for all finalizable objects (including all objects ready for finalization):
12       MT    Count    TotalSize Class Name
13 01954780        1           12 Example_12_1_2.Person2
14 0338c890        1           20 Microsoft.Win32.SafeHandles.SafeFileMappingHandle
15 0338c808        1           20 Microsoft.Win32.SafeHandles.SafeViewOfFileHandle
16 0335b7ac        1           20 Microsoft.Win32.SafeHandles.SafePEFileHandle
17 03389370        2           40 Microsoft.Win32.SafeHandles.SafeFileHandle
18 0335c274        1           44 System.Threading.ReaderWriterLock
19 0335133c        1           52 System.Threading.Thread
20 Total 8 objects

        2.6、如何檢視終端子執行緒。
            偵錯原始碼:Example_12_1_2
            當我們進入調成介面後,【g】繼續執行,程式輸出:分配完畢!我們點選【break】按鈕進入中斷模式,我們直接輸入【!t】或者【!Threads】命令就可以了。
 1 0:000> !t
 2 ThreadCount:      2
 3 UnstartedThread:  0
 4 BackgroundThread: 1
 5 PendingThread:    0
 6 DeadThread:       0
 7 Hosted Runtime:   no
 8                                                                          Lock  
 9        ID OSID ThreadOBJ    State GC Mode     GC Alloc Context  Domain   Count Apt Exception
10    0    1  620 0157a640     2a020 Preemptive  033A4F40:00000000 01542228 1     MTA 
11    5    2 108c 015491a0     2b220 Preemptive  00000000:00000000 01542228 0     MTA (Finalizer) (終端子執行緒)
12 
13 0:000> !threads
14 ThreadCount:      2
15 UnstartedThread:  0
16 BackgroundThread: 1
17 PendingThread:    0
18 DeadThread:       0
19 Hosted Runtime:   no
20                                                                          Lock  
21        ID OSID ThreadOBJ    State GC Mode     GC Alloc Context  Domain   Count Apt Exception
22    0    1  620 0157a640     2a020 Preemptive  033A4F40:00000000 01542228 1     MTA 
23    5    2 108c 015491a0     2b220 Preemptive  00000000:00000000 01542228 0     MTA (Finalizer) (終端子執行緒)
            我們看到紅色標記的 2 號執行緒就是終端子執行緒,如果有物件在【(015859b0->015859b0)】這個區域,終端子執行緒就會被喚起執行。
 1 0:000> !fq
 2 SyncBlocks to be cleaned up: 0
 3 Free-Threaded Interfaces to be released: 0
 4 MTA Interfaces to be released: 0
 5 STA Interfaces to be released: 0
 6 ----------------------------------
 7 generation 0 has 8 finalizable objects (01585990->015859b0)
 8 generation 1 has 0 finalizable objects (01585990->01585990)
 9 generation 2 has 0 finalizable objects (01585990->01585990)
10 Ready for finalization 0 objects (015859b0->015859b0)(如果有物件在這個區間,終端子執行緒才會執行)
11 Statistics for all finalizable objects (including all objects ready for finalization):
12       MT    Count    TotalSize Class Name
13 01954780        1           12 Example_12_1_2.Person2
14 0338c890        1           20 Microsoft.Win32.SafeHandles.SafeFileMappingHandle
15 0338c808        1           20 Microsoft.Win32.SafeHandles.SafeViewOfFileHandle
16 0335b7ac        1           20 Microsoft.Win32.SafeHandles.SafePEFileHandle
17 03389370        2           40 Microsoft.Win32.SafeHandles.SafeFileHandle
18 0335c274        1           44 System.Threading.ReaderWriterLock
19 0335133c        1           52 System.Threading.Thread
20 Total 8 objects

            如果沒有物件在這個區間,終端子執行緒會處於等待狀態。


        2.7、我們檢視一下在具有解構函式的物件被回收的時候,解構函式有沒有被執行。
            偵錯原始碼:Example_12_1_3
            當我們進入調成介面後,【g】繼續執行,程式輸出:開始觸發GC了!我是解構函式。我們點選【break】按鈕進入中斷模式,切換到主執行緒【~0s】,可以把介面清理一下【.cls】。
            我們檢視一下當前的執行緒情況。
 1 0:000> !t
 2 ThreadCount:      2
 3 UnstartedThread:  0
 4 BackgroundThread: 1
 5 PendingThread:    0
 6 DeadThread:       0
 7 Hosted Runtime:   no
 8                                                                          Lock  
 9        ID OSID ThreadOBJ    State GC Mode     GC Alloc Context  Domain   Count Apt Exception
10    0    1 190c 010e3980   202a020 Preemptive  02D36E98:00000000 010dd7c0 0     MTA 
11    5    2 108c 01120738     2b220 Preemptive  02D34B00:00000000 010dd7c0 1     MTA (Finalizer) 

            我們切換到終端子執行緒,執行命令【~~[108c]s

1 0:000> ~~[108c]s
2 eax=00000000 ebx=000000a0 ecx=00000000 edx=00000000 esi=04ecfa68 edi=00000000
3 eip=777c10fc esp=04ecf950 ebp=04ecf9b0 iopl=0         nv up ei pl nz ac pe nc
4 cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000216
5 ntdll!NtReadFile+0xc:
6 777c10fc c22400          ret     24h

            我們檢視一下當前執行緒的呼叫棧。

 1 0:005> !clrstack
 2 OS Thread Id: 0x108c (5)
 3 Child SP       IP Call Site
 4 04ecf9d0 777c10fc [InlinedCallFrame: 04ecf9d0] 
 5 04ecf9cc 052a12db DomainNeutralILStubClass.IL_STUB_PInvoke(Microsoft.Win32.SafeHandles.SafeFileHandle, Byte*, Int32, Int32 ByRef, IntPtr)
 6 04ecf9d0 052ab637 [InlinedCallFrame: 04ecf9d0] Microsoft.Win32.Win32Native.ReadFile(Microsoft.Win32.SafeHandles.SafeFileHandle, Byte*, Int32, Int32 ByRef, IntPtr)
 7 04ecfa34 052ab637 System.IO.__ConsoleStream.ReadFileNative(Microsoft.Win32.SafeHandles.SafeFileHandle, Byte[], Int32, Int32, Boolean, Boolean, Int32 ByRef)
 8 04ecfa68 052ab4d9 System.IO.__ConsoleStream.Read(Byte[], Int32, Int32)
 9 04ecfa88 052ab3b3 System.IO.StreamReader.ReadBuffer()
10 04ecfa98 052ab178 System.IO.StreamReader.ReadLine()
11 04ecfab4 052ab129 System.IO.TextReader+SyncTextReader.ReadLine() [f:\dd\ndp\clr\src\BCL\system\io\textreader.cs @ 363]
12 04ecfac4 052aa873 System.Console.ReadLine()
13 04ecfacc 052aa425 Example_12_1_3.Person.Finalize() [E:\Visual Studio 2022\Example_12_1_3\Person.cs @ 11]
14 04ecfce8 6e4b13b4 [DebuggerU2MCatchHandlerFrame: 04ecfce8] 

            執行了 Person 的 Finalize()方法。為什麼不是解構函式呢?不過是一個語法糖。這個方法會被終端子執行緒持有,並被呼叫執行清理工作。千萬注意不要讓解構函式卡死,如果導致解構函式卡死,就會導致終端子執行緒卡死,所有具有解構函式的物件都無法執行清理的工作,記憶體暴漲。


        2.8、我們檢視大物件堆的 Free 塊。
            偵錯原始碼:Example_12_1_4
            當我們進入調成介面後,【g】繼續執行,程式輸出:1、物件已經分配,請檢視託管堆!。在【Debugger.Break()】這行程式碼進入中斷模式。由於我們分配的都是大物件,所以直接檢視大物件堆,執行命令【!eeheap -gc】。
 1 0:000> !eeheap -gc
 2 Number of GC Heaps: 1
 3 generation 0 starts at 0x02f51018
 4 generation 1 starts at 0x02f5100c
 5 generation 2 starts at 0x02f51000
 6 ephemeral segment allocation context: none
 7  segment     begin  allocated      size
 8 02f50000  02f51000  02f55ff4  0x4ff4(20468)
 9 Large object heap starts at 0x03f51000
10  segment     begin  allocated      size
11 03f50000  03f51000  040265b0  0xd55b0(873904)
12 Total Size:              Size: 0xda5a4 (894372) bytes.
13 ------------------------------
14 GC Heap Size:    Size: 0xda5a4 (894372) bytes.

          03f51000 040265b0 紅色標註的就是大物件堆 Segment 開始和結束區間,我們通過【!dumpheap 03f51000 040265b0】檢視一下這個 LOH 裡有什麼。

 1 0:000> !dumpheap 03f51000  040265b0
 2  Address       MT     Size
 3 03f51000 01165470       10 Free
 4 03f51010 01165470       14 Free
 5 03f51020 02dda2fc     4872     
 6 03f52328 01165470       14 Free
 7 03f52338 02dda2fc      524     
 8 03f52548 01165470       14 Free
 9 03f52558 02dda2fc     8172     
10 03f54548 01165470       14 Free
11 03f54558 02dda2fc     4092     
12 03f55558 01165470       14 Free
13 03f55568 02e07054   185012     
14 03f82820 01165470       14 Free
15 03f82830 02e07054   285012     (這裡就是我們 var byte2 = new byte[285000]分配的物件)
16 03fc8188 01165470       14 Free
17 03fc8198 02e07054   385012     
18 04026190 01165470       14 Free
19 040261a0 02dda2fc     1036     
20 
21 Statistics:
22       MT    Count    TotalSize Class Name
23 01165470        9          122      Free
24 02dda2fc        5        18696 System.Object[]

            我們繼續【g】一下,我們的程式輸出:2、GC 已經觸發,請檢視託管堆中的 byte2。說明 byte2 物件已經被回收,也就是上面標記的物件是一個 Free 塊了。

 1 0:000> !dumpheap 03f51000  040265b0
 2  Address       MT     Size
 3 03f51000 01165470       10 Free
 4 03f51010 01165470       14 Free
 5 03f51020 02dda2fc     4872     
 6 03f52328 01165470       14 Free
 7 03f52338 02dda2fc      524     
 8 03f52548 01165470       14 Free
 9 03f52558 02dda2fc     8172     
10 03f54548 01165470       14 Free
11 03f54558 02dda2fc     4092     
12 03f55558 01165470       14 Free
13 03f55568 02e07054   185012     
14 03f82820 01165470   285046 Free(變成 Free 塊了)
15 03fc8198 02e07054   385012     
16 04026190 01165470       14 Free
17 040261a0 02dda2fc     1036     
18 
19 Statistics:
20       MT    Count    TotalSize Class Name
21 02dda2fc        5        18696 System.Object[]
22 01165470        8       285140      Free
23 02e07054        2       570024 System.Byte[]
24 Total 15 objects

            我們繼續【g】一下,會重新分配 byte4 = new byte[280000] 物件。我們的程式輸出:3、已分配 byte4,檢視是否 Free 塊中。我們再次檢視大物件堆,看看發生了什麼變化。

 1 0:000> !dumpheap 03f51000  040265b0
 2  Address       MT     Size
 3 03f51000 01165470       10 Free
 4 03f51010 01165470       14 Free
 5 03f51020 02dda2fc     4872     
 6 03f52328 01165470       14 Free
 7 03f52338 02dda2fc      524     
 8 03f52548 01165470       14 Free
 9 03f52558 02dda2fc     8172     
10 03f54548 01165470       14 Free
11 03f54558 02dda2fc     4092     
12 03f55558 01165470       14 Free
13 03f55568 02e07054   185012     
14 03f82820 01165470       14 Free
15 03f82830 02e07054   280012  (我們重新分配的 byte4 物件)   
16 03fc6e00 01165470     5014 Free
17 03fc8198 02e07054   385012     
18 04026190 01165470       14 Free
19 040261a0 02dda2fc     1036     
20 
21 Statistics:
22       MT    Count    TotalSize Class Name
23 01165470        9         5122      Free
24 02dda2fc        5        18696 System.Object[]
25 02e07054        3       850036 System.Byte[]
26 Total 17 objects
        紅色標註的已經說明了問題,分配的 byte4 物件大小正好在 Free 塊中,所以就把 byte4 直接儲存了。

四、總結
    終於寫完了。還是老話,雖然很忙,寫作過程也挺累的,但是看到了自己的成長,心裡還是挺快樂的。學習過程真的沒那麼輕鬆,還好是自己比較喜歡這一行,否則真不知道自己能不能堅持下來。老話重談,《高階偵錯》的這本書第一遍看,真的很暈,第二遍稍微好點,不學不知道,一學嚇一跳,自己欠缺的很多。好了,不說了,不忘初心,繼續努力,希望老天不要辜負努力的人。