Net 高级调试之十二:垃圾回收机制以及终结器队列、对象固定

发布时间 2023-12-08 13:45:25作者: 可均可可
一、简介
    今天是《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 直接存储了。

四、总结
    终于写完了。还是老话,虽然很忙,写作过程也挺累的,但是看到了自己的成长,心里还是挺快乐的。学习过程真的没那么轻松,还好是自己比较喜欢这一行,否则真不知道自己能不能坚持下来。老话重谈,《高级调试》的这本书第一遍看,真的很晕,第二遍稍微好点,不学不知道,一学吓一跳,自己欠缺的很多。好了,不说了,不忘初心,继续努力,希望老天不要辜负努力的人。