Net 高級調試之十二:垃圾回收機制以及終結器隊列、對象固定

一、簡介
    今天是《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 直接存儲了。

四、總結
    終於寫完了。還是老話,雖然很忙,寫作過程也挺累的,但是看到了自己的成長,心裏還是挺快樂的。學習過程真的沒那麼輕鬆,還好是自己比較喜歡這一行,否則真不知道自己能不能堅持下來。老話重談,《高級調試》的這本書第一遍看,真的很暈,第二遍稍微好點,不學不知道,一學嚇一跳,自己欠缺的很多。好了,不說了,不忘初心,繼續努力,希望老天不要辜負努力的人。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章