本項目案例後臺採用.NET6(C#)開發,前端採用React&React Native,數字孿生採用3DMAX&U3D。綜合運用“物、大、智、雲、移”技術,採用雲-邊-端工業互聯網架構,設備端基於工業感測器和物聯網保障動態感知,邊緣側基於工藝機理、專家知識、數據科學等多種技術手段實現工況診斷,大數據... ...
一、簡介
今天是《Net 高級調試》的第十一篇文章,這篇文章來的有點晚,因為,最近比較忙,就沒時間寫文章了。現在終於有點時間,繼續開始我們這個系列。這篇文章我們主要介紹托管堆的架構,對象的分配機制,我們如何查找在托管堆上的對象,我學完這章,很多以前很模糊的概念,現在很清晰了,知道了對象代的分配,大對象堆和小對象堆的結構,瞭解了對象的生命周期,這些是 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、Windows 記憶體架構
要瞭解 C# 的記憶體分配機制,首先需要瞭解 Windows 記憶體分配的機制,畢竟 CLR 中的記憶體是從 Windows 上分配過來的。架構圖如下:
2、CLR堆管理器
2.1、簡介
CLR 堆管理器托管的記憶體劃分成兩大塊。
a、按對象大小劃分
所有小於 85000 byte 的對象都存放在【小對象堆(SOH)】,大於等於 85000 byte 的對象存放在【大對象堆(BOH)】。
b、按生存期劃分。
CLR 假設一個新分配的對象往往更容易成為一個垃圾對象,所以回收這些對象的效率會更高,所以在【小對象堆(SOH)】做了一個【代機制】的劃分,也就是 0代、1代、2代。
2.2、托管堆佈局圖
托管堆是由很多的段(segment)組成的,新生成的 segment 叫做臨時段,其他的叫 segment 年長段。小對象堆有臨時段的概念,大對象堆是沒有的。結構如圖:
托管堆是由段組成的,在小對象堆中,最新創建的是臨時段,其他則是記憶體段,依次類推。所有的0代對象和1代對象都會分配在臨時段上,2代對象會有一部分放在臨時段上,其他的段,比如:記憶體段,永遠存放的是2代對象。
2.3、工作站和伺服器GC
CLR 有兩種 GC 模式,分別是:工作站 GC 和 伺服器GC。
a)、工作站GC
工作站GC 一般指具有視窗類的應用程式,比如:WinForm,WPF,SliverLight,Console,這樣的程式只有一個托管堆。
效果如圖:
b)、伺服器GC
對於 Web 類的程式,一般預設使用 伺服器GC,它的托管堆個數和當前機器 CPU 的核數一致。
如圖:
3、對象分配
當我們在對象堆中分配一個對象時,大致流程如下:
a)、將一個對象分配到托管堆上。
b)、如果托管堆的空間不足,將會觸發 GC。
c)、GC 觸發之後,如果空間足夠的話,就會存放對象。
d)、如果垃圾對象帶有析構函數,那麼將會進入到【可終結隊列】中被執行。
過程如圖:
4、dumpheap 命令介紹
為了能夠高效的篩選托管堆中的對象,SOS 提供了一個【!dumpheap】命令,這個命令十分強大,可以幫助我們很方便的篩選。
三、調試過程
廢話不多說,這一節是具體的調試過程,又可以說是眼見為實的過程,在開始之前,我還是要啰嗦兩句,這一節分為兩個部分,第一部分是測試的源碼部分,沒有代碼,當然就談不上測試了,調試必須有載體。第二部分就是根據具體的代碼來證實我們學到的知識,是具體的眼見為實。
1、調試源碼
1.1、Example_11_1_1
1 namespace Example_11_1_1 2 { 3 internal class Program 4 { 5 static void Main(string[] args) 6 { 7 byte[] byte1 = new byte[10000]; 8 byte[] byet2 = new byte[85000]; 9 Console.WriteLine("Hello world!"); 10 Console.ReadLine(); 11 } 12 } 13 }View Code
1.2、Example_11_1_2
這個項目很簡單,就是建立一個 Asp.Net WebAPI 的項目,不需要寫任何代碼,使用預設功能就可以。
1.3、Example_11_1_3
1 namespace Example_11_1_3 2 { 3 internal class Program 4 { 5 static void Main(string[] args) 6 { 7 byte[] byte1 = new byte[85000]; 8 byte[] byte2 = new byte[1500]; 9 byte[] byet3 = new byte[3500]; 10 Console.WriteLine("3 個 byte[] 分配完畢!"); 11 Console.ReadLine(); 12 } 13 } 14 }View Code
2、眼見為實
項目的所有操作都是一樣的,所以就在這裡說明一下,但是每個測試例子,都需要重新啟動,並載入相應的應用程式,載入方法都是一樣的。流程如下:我們編譯項目,打開 Windbg,點擊【文件】----》【launch executable】附加程式,打開調試器的界面,程式已經處於中斷狀態。
2.1、我們查看 NT 堆和 GC 堆。
調試源碼:Example_11_1_1
1)、我們先來查看一下 NT 堆。
當我們成功進入調試器界面,使用【g】命令,繼續運行,我們會在12行代碼【Console.ReadLine()】暫停,我們程式列印出了【Hello world】,我們點擊調試器工具欄中的【break】按鈕,就可以調試程式了。
我們使用【!address -summary 】命令,查看一下具體情況。
1 0:006> !address -summary 2 3 4 Mapping file section regions... 5 Mapping module regions... 6 Mapping PEB regions... 7 Mapping TEB and stack regions... 8 Mapping heap regions... 9 Mapping page heap regions... 10 Mapping other regions... 11 Mapping stack trace database regions... 12 Mapping activation context regions... 13 14 --- Usage Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal 15 Free 57 f852b000 ( 3.880 GB) 97.00% 16 Image 191 2eb9000 ( 46.723 MB) 38.06% 1.14% 17 <unknown> 88 2670000 ( 38.438 MB) 31.31% 0.94% 18 MappedFile 18 1cec000 ( 28.922 MB) 23.56% 0.71% 19 Stack 21 700000 ( 7.000 MB) 5.70% 0.17% 20 Heap 10 13b000 ( 1.230 MB) 1.00% 0.03% 21 Other 7 5c000 ( 368.000 kB) 0.29% 0.01% 22 TEB 7 16000 ( 88.000 kB) 0.07% 0.00% 23 PEB 1 3000 ( 12.000 kB) 0.01% 0.00% 24 25 --- Type Summary (for busy) ------ RgnCount ----------- Total Size -------- %ofBusy %ofTotal 26 MEM_IMAGE 198 2ec3000 ( 46.762 MB) 38.09% 1.14% 27 MEM_PRIVATE 122 2ebc000 ( 46.734 MB) 38.07% 1.14% 28 MEM_MAPPED 23 1d46000 ( 29.273 MB) 23.84% 0.71% 29 30 --- State Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal 31 MEM_FREE 57 f852b000 ( 3.880 GB) 97.00% 32 MEM_RESERVE 65 43a4000 ( 67.641 MB) 55.10% 1.65% 33 MEM_COMMIT 278 3721000 ( 55.129 MB) 44.90% 1.35% 34 35 --- Protect Summary (for commit) - RgnCount ----------- Total Size -------- %ofBusy %ofTotal 36 PAGE_EXECUTE_READ 32 25b2000 ( 37.695 MB) 30.70% 0.92% 37 PAGE_READONLY 90 bae000 ( 11.680 MB) 9.51% 0.29% 38 PAGE_WRITECOPY 30 399000 ( 3.598 MB) 2.93% 0.09% 39 PAGE_READWRITE 102 1f8000 ( 1.969 MB) 1.60% 0.05% 40 PAGE_READWRITE | PAGE_GUARD 16 28000 ( 160.000 kB) 0.13% 0.00% 41 PAGE_EXECUTE_READWRITE 8 8000 ( 32.000 kB) 0.03% 0.00% 42 43 --- Largest Region by Usage ----------- Base Address -------- Region Size ---------- 44 Free 80010000 7f2f0000 ( 1.987 GB) 45 Image 6de80000 f55000 ( 15.332 MB) 46 <unknown> 2f22000 fee000 ( 15.930 MB) 47 MappedFile 19d4000 133d000 ( 19.238 MB) 48 Stack 1410000 fd000 (1012.000 kB) 49 Heap f20000 8b000 ( 556.000 kB) 50 Other ff480000 33000 ( 204.000 kB) 51 TEB ce1000 4000 ( 16.000 kB) 52 PEB ccc000 3000 ( 12.000 kB)View Code
輸出的內容還是不少的,列表中【Heap 10 13b000 ( 1.230 MB) 1.00% 0.03%】,這個就是 NT 堆。
我們也可以使用【!heap -s】命令,查看 NT 堆的詳情。
1 0:006> !heap -s 2 3 4 ************************************************************************************************************************ 5 NT HEAP STATS BELOW 6 ************************************************************************************************************************ 7 NtGlobalFlag enables following debugging aids for new heaps: 8 tail checking 9 free checking 10 validate parameters 11 LFH Key : 0x0afd8ea9 12 Termination on corruption : ENABLED 13 Heap Flags Reserv Commit Virt Free List UCR Virt Lock Fast 14 (k) (k) (k) (k) length blocks cont. heap 15 ----------------------------------------------------------------------------- 16 00f20000 40000062 1020 556 1020 6 65 1 0 4 (第一個堆是進程堆,是 Win32函數用的) 17 01200000 40001062 60 12 60 1 2 1 0 0 18 01570000 40001062 60 12 60 1 2 1 0 0 19 02f00000 40001062 60 4 60 0 1 1 0 0 20 01540000 40041062 60 4 60 2 1 1 0 0 21 -----------------------------------------------------------------------------
2)、我們查看一下 GC 堆。
如果我們想查看 GC 堆,可以直接使用【!eeheap -gc】命令。
1 0:006> !eeheap -gc 2 Number of GC Heaps: 1 3 generation 0 starts at 0x02f11018 4 generation 1 starts at 0x02f1100c 5 generation 2 starts at 0x02f11000 6 ephemeral segment allocation context: none 7 segment begin allocated size 8 02f10000 02f11000 02f1871c 0x771c(30492) 9 Large object heap starts at 0x03f11000 10 segment begin allocated size 11 03f10000 03f11000 03f2a180 0x19180(102784) 12 Total Size: Size: 0x2089c (133276) bytes. 13 ------------------------------ 14 GC Heap Size: Size: 0x2089c (133276) bytes.
2.2、我們查看大對象和小對象分配機制
調試源碼:Example_11_1_1
當我們成功進入調試器界面,使用【g】命令,繼續運行,我們會在12行代碼【Console.ReadLine()】暫停,我們程式列印出了【Hello world】,我們點擊調試器工具欄中的【break】按鈕,就可以調試程式了。
byte[] byte1 = new byte[10000];這行數組小於85000就應該在小對象堆中,byte[] byet2 = new byte[85000];這個數組對象大於85000 就會分配在大對象堆中。
我們先使用【!eeheap -gc】查看一下托管堆的情況。
1 0:006> !eeheap -gc 2 Number of GC Heaps: 1 3 generation 0 starts at 0x02f11018 4 generation 1 starts at 0x02f1100c 5 generation 2 starts at 0x02f11000 6 ephemeral segment allocation context: none 7 segment begin allocated size 8 02f10000 02f11000 02f1871c 0x771c(30492) 9 Large object heap starts at 0x03f11000(這個就是大對象堆) 10 segment begin allocated size 11 03f10000 03f11000 03f2a180 0x19180(102784) 12 Total Size: Size: 0x2089c (133276) bytes. 13 ------------------------------ 14 GC Heap Size: Size: 0x2089c (133276) bytes.
我們再使用【!dumpheap 03f11000 03f2a180】查看一下大對象堆的情況。
0:006> !dumpheap 03f11000 03f2a180 Address MT Size 03f11000 00f45368 10 Free 03f11010 00f45368 14 Free 03f11020 6dae2788 4872 03f12328 00f45368 14 Free 03f12338 6dae2788 524 03f12548 00f45368 14 Free 03f12558 6dae2788 4092 03f13558 00f45368 14 Free 03f13568 6dae2788 8172 03f15558 00f45368 14 Free 03f15568 6dae5c40 85012 Statistics: MT Count TotalSize Class Name 00f45368 6 80 Free 6dae2788 4 17660 System.Object[] 6dae5c40 1 85012 System.Byte[] Total 11 objects
紅色標註的就是我們的 byte 數組,大小是 85012,為什麼多了12,數組是引用類型,引用類型都有兩個附加欄位(8)和一個數組長度(4)的欄位,共12,再加上數組的長度就是85012。
我們確認了 byet2 確實是在大對象堆中,我們繼續看看 byet1是不是在小對象堆中。使用相同的命令【!dumpheap 02f11000 02f1871c】,這個地址就是小對象堆的起始地址和結束地址,就是【ephemeral segment allocation】臨時段。
1 0:006> !dumpheap 02f11000 02f1871c 2 Address MT Size 3 02f11000 00f45368 10 Free 4 02f1100c 00f45368 10 Free 5 ..... 6 02f124fc 00f45368 10 Free 7 02f12508 6dae5c40 10012 8 02f14c24 6dae8b20 28 9 ..... 10 02f174f4 6db49b0c 16 11 12 Statistics: 13 MT Count TotalSize Class Name 14 ...... 15 6dae5c40 4 10818 System.Byte[] 16 Total 325 objects
紅色標記的就是 byte 數組,我們使用【!DumpHeap /d -mt 6dae5c40】查看詳情。
1 0:006> !DumpHeap /d -mt 6dae5c40 2 Address MT Size 3 02f12508 6dae5c40 10012 4 02f16710 6dae5c40 526 5 02f171cc 6dae5c40 268 6 02f174e8 6dae5c40 12 7 03f15568 6dae5c40 85012 8 9 Statistics: 10 MT Count TotalSize Class Name 11 6dae5c40 5 95830 System.Byte[] 12 Total 5 objects
紅色標記的就是我們的 byte1 byte 數組。當然,我們可以使用命令【!do】查看詳情。
1 0:006> !do 02f12508 2 Name: System.Byte[] 3 MethodTable: 6dae5c40 4 EEClass: 6dbe8ba8 5 Size: 10012(0x271c) bytes 6 Array: Rank 1, Number of elements 10000, Type Byte (Print Array) 7 Content: ................................................................................................................................ 8 Fields: 9 None
2.3、如何按生存期查看對象的分配。
調試源碼:Example_11_1_1
當我們成功進入調試器界面,使用【g】命令,繼續運行,我們會在12行代碼【Console.ReadLine()】暫停,我們程式列印出了【Hello world】,我們點擊調試器工具欄中的【break】按鈕,就可以調試程式了。
其實,有關對象代的調試,我們已經做過了,這裡正式測試一下,我們依然使用【!eeheap -gc】命令,就可以看到托管堆中的代了。
1 0:006> !eeheap -gc 2 Number of GC Heaps: 1 3 generation 0 starts at 0x02f11018(0代) 4 generation 1 starts at 0x02f1100c(1代) 5 generation 2 starts at 0x02f11000(2代) 6 ephemeral segment allocation context: none 7 segment begin allocated size 8 02f10000 02f11000 02f1871c 0x771c(30492) 9 Large object heap starts at 0x03f11000 10 segment begin allocated size 11 03f10000 03f11000 03f2a180 0x19180(102784) 12 Total Size: Size: 0x2089c (133276) bytes. 13 ------------------------------ 14 GC Heap Size: Size: 0x2089c (133276) bytes.
2.4、查看 Console GC 模式
調試源碼:Example_11_1_1
當我們成功進入調試器界面,使用【g】命令,繼續運行,我們會在12行代碼【Console.ReadLine()】暫停,我們程式列印出了【Hello world】,我們點擊調試器工具欄中的【break】按鈕,就可以調試程式了。
我們可以使用【!eeversion】命令,查看GC模式。
1 0:006> !eeversion 2 4.8.4300.0 retail 3 Workstation mode(工作站模式) 4 SOS Version: 4.8.4300.0 retail build
我們也可以通過【!eeheap -gc】命令查看托管堆的個數查看。
1 0:006> !eeheap -gc 2 Number of GC Heaps: 1(只有一個托管堆,就是工作站模式) 3 generation 0 starts at 0x02f11018 4 generation 1 starts at 0x02f1100c 5 generation 2 starts at 0x02f11000 6 ephemeral segment allocation context: none 7 segment begin allocated size 8 02f10000 02f11000 02f1871c 0x771c(30492) 9 Large object heap starts at 0x03f11000 10 segment begin allocated size 11 03f10000 03f11000 03f2a180 0x19180(102784) 12 Total Size: Size: 0x2089c (133276) bytes. 13 ------------------------------ 14 GC Heap Size: Size: 0x2089c (133276) bytes.
2.5、查看 Asp.Net Web API 的 GC 模式
調試源碼:Example_11_1_2
這裡測試的源碼時 Web API 項目,直接運行程式,然後我們通過 Windbg 的【attach to Process】命令來查看。附加進程的進程是【iisexpress】,效果如圖:
如果你使用的是【Debug】運行的 WEBAPI,調試會失敗,附加進程有誤,如圖:
在 Windbg中提示的具體錯誤:"The process that you are attempting to attach to is already being debugged. Only one debugger can be invasively attached to a process at a time. A non-invasive attach is still possible when another debugger is attached." ,意思就是:嘗試附加到的進程已在調試中。一次只能將一個調試器侵入性附加到進程。把程式的運行模式改為【Release】模式,不用使用調試模式,快捷鍵:Ctrl+F5,就可以附加進程成功了。
我們可以使用【!eeheap -gc】命令查看一下伺服器GC模式。
1 0:037> !eeheap -gc 2 Number of GC Heaps: 4(有四個堆,這既是伺服器GC模式) 3 ------------------------------ 4 Heap 0 (000001ff28a2dc70) 5 generation 0 starts at 0x000001ff29283e08 6 generation 1 starts at 0x000001ff29121018 7 generation 2 starts at 0x000001ff29121000 8 ephemeral segment allocation context: none 9 segment begin allocated size 10 000001ff29120000 000001ff29121000 000001ff2a79a728 0x1679728(23566120) 11 Large object heap starts at 0x0000020329121000 12 segment begin allocated size 13 0000020329120000 0000020329121000 000002032941f2c0 0x2fe2c0(3138240) 14 Heap Size: Size: 0x19779e8 (26704360) bytes. 15 ------------------------------ 16 Heap 1 (000001ff28a5dc40) 17 generation 0 starts at 0x000002002923b6d0 18 generation 1 starts at 0x0000020029121018 19 generation 2 starts at 0x0000020029121000 20 ephemeral segment allocation context: none 21 segment begin allocated size 22 0000020029120000 0000020029121000 000002002a755fe8 0x1634fe8(23285736) 23 Large object heap starts at 0x0000020339121000 24 segment begin allocated size 25 0000020339120000 0000020339121000 00000203392c8ff0 0x1a7ff0(1736688) 26 Heap Size: Size: 0x17dcfd8 (25022424) bytes. 27 ------------------------------ 28 Heap 2 (000001ff28a87bf0) 29 generation 0 starts at 0x00000201291ebe00 30 generation 1 starts at 0x0000020129121018 31 generation 2 starts at 0x0000020129121000 32 ephemeral segment allocation context: none 33 segment begin allocated size 34 0000020129120000 0000020129121000 000002012993ffe8 0x81efe8(8515560) 35 Large object heap starts at 0x0000020349121000 36 segment begin allocated size 37 0000020349120000 0000020349121000 0000020349121018 0x18(24) 38 Heap Size: Size: 0x81f000 (8515584) bytes. 39 ------------------------------ 40 Heap 3 (000001ff28ab1ba0)