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 }
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 }
Person1 类源码:


1 internal class Person1
2 {
3 }
Person2 类源码:


1 internal class Person2
2 {
3 ~Person2()
4 {
5 Console.WriteLine("我是析构函数");
6 }
7 }
Person3 类源码:


1 internal class Person3
2 {
3 }
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 }
Person 类源码:


1 internal class Person
2 {
3 ~Person()
4 {
5 Console.WriteLine("我是析构函数");
6
7 Console.ReadLine();
8 }
9 }
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 }
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 直接存储了。
四、总结
终于写完了。还是老话,虽然很忙,写作过程也挺累的,但是看到了自己的成长,心里还是挺快乐的。学习过程真的没那么轻松,还好是自己比较喜欢这一行,否则真不知道自己能不能坚持下来。老话重谈,《高级调试》的这本书第一遍看,真的很晕,第二遍稍微好点,不学不知道,一学吓一跳,自己欠缺的很多。好了,不说了,不忘初心,继续努力,希望老天不要辜负努力的人。
Net 高级调试之十二:垃圾回收机制以及终结器队列、对象固定的更多相关文章
- JVM虚拟机-垃圾回收机制与垃圾收集器概述
目录 前言 什么是垃圾回收 垃圾回收的区域 垃圾回收机制 流程 怎么判断对象已经死亡 引用计数法 可达性分析算法 不可达的对象并非一定会回收 关于引用 强引用(StrongReference) 软引用 ...
- js的垃圾回收机制
Js具有自动垃圾回收机制.垃圾收集器会按照固定的时间间隔周期性的执行. JS中最常见的垃圾回收方式是标记清除. 工作原理:是当变量进入环境时,将这个变量标记为“进入环境”.当变量离开环境时,则将其标记 ...
- 关于JS垃圾回收机制
一.垃圾回收机制的必要性 由于字符串.对象和数组没有固定大小,所以当它们的大小已知时,才能对它们进行动态的存储分配.JavaScript程序每次创建字符串.数组或对象时,解释器都必须分配内存来存储那个 ...
- python垃圾回收机制:引用计数 VS js垃圾回收机制:标记清除
js垃圾回收机制:标记清除 Js具有自动垃圾回收机制.垃圾收集器会按照固定的时间间隔周期性的执行. JS中最常见的垃圾回收方式是标记清除. 工作原理 当变量进入环境时,将这个变量标记为"进入 ...
- JVM(十),垃圾回收之新生代垃圾收集器
十.垃圾回收之新生代垃圾收集器 1.JVM的运行模式 2.Serial收集器(复制算法-单线程-Client模式) 2.ParNew收集器(复制算法-多线程-Client模式) 3.Parallel ...
- JVM的生命周期、体系结构、内存管理和垃圾回收机制
一.JVM的生命周期 JVM实例:一个独立运行的java程序,是进程级别 JVM执行引擎:用户运行程序的线程,是JVM实例的一部分 JVM实例的诞生 当启动一个java程序时.一个JVM实例就诞生了, ...
- 【转】深入理解 Java 垃圾回收机制
深入理解 Java 垃圾回收机制 一.垃圾回收机制的意义 Java语言中一个显著的特点就是引入了垃圾回收机制,使c++程序员最头疼的内存管理的问题迎刃而解,它使得Java程序员在编写程序的时候不再 ...
- 深入理解java垃圾回收机制
深入理解java垃圾回收机制---- 一.垃圾回收机制的意义 Java语言中一个显著的特点就是引入了垃圾回收机制,使c++程序员最头疼的内存管理的问题迎刃而解,它使得Java程序员在编写程序的时候不再 ...
- 深入理解 Java 垃圾回收机制
深入理解 Java 垃圾回收机制 一:垃圾回收机制的意义 java 语言中一个显著的特点就是引入了java回收机制,是c++程序员最头疼的内存管理的问题迎刃而解,它使得java程序员 ...
- 大数据基础篇----jvm的知识点归纳-5个区和垃圾回收机制
一直对jvm看了又忘,忘了又看的.今天做一个笔记整理存放在这里. 我们先看一下JVM的内存模型图: 上面有5个区,这5个区干嘛用的呢? 我们想象一个场景: 我们有一个class文件,里面有很多的类的定 ...
随机推荐
- SElinux 导致 Keepalived 检测脚本无法执行
哈喽大家好,我是咸鱼 今天我们来看一个关于 Keepalived 检测脚本无法执行的问题 一位粉丝后台私信我,说他部署的 keepalived 集群 vrrp_script 模块中的脚本执行失败了,但 ...
- 一文了解 history 和 react-router 的实现原理
我们是袋鼠云数栈 UED 团队,致力于打造优秀的一站式数据中台产品.我们始终保持工匠精神,探索前端道路,为社区积累并传播经验价值. 本文作者:霜序 前言 在前一篇文章中,我们详细的说了 react-r ...
- 音视频FAQ(一):视频直播卡顿
一.摘要 本文介绍了视频直播卡顿的四个主要原因,用户网络问题.用户设备性能问题.技术路线的选择和实现问题.因本文主要阐述视频直播的卡顿,故技术路线的实现指的是:CDN供应商的实现问题,包含CDN性能不 ...
- 《代码整洁之道 Clean Code》学习笔记 Part 1
前段时间在看<架构整洁之道>,里面提到了:构建一个好的软件系统,应该从写整洁代码做起.毕竟,如果建筑使用的砖头质量不佳,再好的架构也无法造就高质量的建筑.趁热打铁,翻出<代码整洁之道 ...
- 原来你是这样的SpringBoot--初识SpringBootAdmin
简介 Spring Boot Admin(SBA)是一个针对spring-boot的actuator接口进行UI美化封装的监控工具.它可以:在列表中浏览所有被监控spring-boot项目的基本信息, ...
- 2015-CS
2015-CS 数据库部分 create table [EMPLOYEE]( [EmpNo] varchar(10) not null primary key, [EmpName] varchar(1 ...
- 各种SQL连接符Join
一.连接符分类,内连接,外连接 1.内连接:Inner Join简写Join. 2.外连接:Left Outer Join 简写Left Join:Right Outer Join 简写Right J ...
- 聊聊JDK19特性之虚拟线程
1.前言 在读<深入理解JVM虚拟机>这本书前两章的时候整理了JDK从1.0到最新版本发展史,其中记录了JDK这么多年来演进过程中的一些趣闻及引人注目的一些特性,在调研JDK19新增特性的 ...
- nacos-mysql.sql
https://github.com/alibaba/nacos/blob/master/distribution/conf/nacos-mysql.sql # Nacos安装 /home/nacos ...
- 深入了解 GPU 互联技术——NVLINK
随着人工智能和图形处理需求的不断增长,多 GPU 并行计算已成为一种趋势.对于多 GPU 系统而言,一个关键的挑战是如何实现 GPU 之间的高速数据传输和协同工作.然而,传统的 PCIe 总线由于带宽 ...