秘境探索之一个.NET 对象从内存分配到内存回收
前方高能預(yù)警,新手慎入!不聽勸阻者,輕則郁悶堆積,重則生死看淡,對編程失去了念想,對生活失去了幻想!好了,心理強大到NB的可以忽略前方若干警示。為了探索.NET對象的內(nèi)存分配和回收銷毀,您可能需要準備一些調(diào)試的基本知識,比如上篇的<利用SOS擴展庫進入高階.NET6程序的調(diào)試>.以下例子來自.net 6技術(shù)支持。
1. 我們的第一個對象
我們的第一個對象,不是你初中暗戀的古靈精怪的小女孩,更不是你高中的神秘御姐范的初戀女友,她是地地道道的Object。
不信,我Show給你看。
public static int Main() {MaoniType o = new MaoniType(128, 256);Console.ReadLine();// 其它亂七八糟的代碼return 0; }掀開她神秘的蓋頭,她也只不是千千萬萬普通對象中的一員,非要說她有什么不同的話,那可能就是你想馴服她,并且你花費了你的寶貴時間,在她身上。
public class MaoniType {public MaoniType(int a, int b){A = a;B = b;}public int A { get; set; }public int B { get; set; } }2. 正確的打開她
美麗總是隱藏在朦朧之中,隔紗看美人,越看越迷人。
不過我們需要的不是膚淺的撩騷,讓我們利用高級窺探工具,更加深入到靈魂的探索她。
當然,最最簡單的探索工具,就是Windbg + SoS 擴展了。
至于工具的使用,不是重點,在這里就略過了,如果你還不會的話,那么就移步<利用SOS擴展庫進入高階.NET6程序的調(diào)試>瞧瞧,那里已經(jīng)給你備好了下酒好菜。
閑話少敘,讓我們直接打開工具,鍵入神秘指令,來個一指入魂吧。
0:007> .load C:\Users\webmote.dotnet\sos\sos.dll 0:007> !dumpheap -stat Statistics:MT Count TotalSize Class Name 00007ffc77c37598 1 24 System.IO.SyncTextReader 00007ffc77c33478 1 24 System.Threading.Tasks.Task+<>c 00007ffc77c1ca70 1 24 System.IO.Stream+NullStream 00007ffc77c13798 1 24 ConsoleApp6.MaoniType ... [omitted]00007ffc77bd7f48 28 1160 System.SByte[] 00007ffc77bd8410 4 3596 System.Int32[] 00007ffc77c1d3c8 3 4178 System.Byte[] 00007ffc77b2b578 8 18216 System.Object[] 00007ffc77c33898 3 33356 System.Char[] 00007ffc77bdd698 82 35610 System.String Total 208 objects沒錯,找到?ConsoleApp6.MaoniType?這個類名,這就是你心心念的?對象?No 1.
3. 深入內(nèi)存
既然已經(jīng)被你定位到了,那么就讓我們繼續(xù)深入吧, 現(xiàn)在只需要點她的牌牌就可以了。
0:007> !DumpHeap /d -mt 00007ffc77c13798Address MT Size 000002470000c0c8 00007ffc77c13798 24 Statistics:MT Count TotalSize Class Name 00007ffc77c13798 1 24 ConsoleApp6.MaoniType Total 1 objects現(xiàn)在,有了她第一手的資訊:
姓名:Maoni/莫妮 尺寸:24 起點:c0c8 [000002470000c0c8] 個數(shù):1個 表索引:[00007ffc77c13798]4. 繼續(xù)深入——內(nèi)存布局調(diào)查
讓我們來看看GC地址空間的情況:
0:007> !eeheap -gc Number of GC Heaps: 1 generation 0 starts at 0x0000024700001030 generation 1 starts at 0x0000024700001018 generation 2 starts at 0x0000024700001000 ephemeral segment allocation context: nonesegment begin allocated committed allocated size committed size 0000024700000000 0000024700001000 00000247000173C8 0000024700022000 0x163c8(91080) 0x21000(135168) Large object heap starts at 0x0000024710001000segment begin allocated committed allocated size committed size 0000024710000000 0000024710001000 0000024710001018 0000024710002000 0x18(24) 0x1000(4096) Pinned object heap starts at 0x0000024718001000 0000024718000000 0000024718001000 0000024718005420 0000024718012000 0x4420(17440) 0x11000(69632) Total Allocated Size: Size: 0x1a800 (108544) bytes. Total Committed Size: Size: 0x22000 (139264) bytes. ------------------------------ GC Allocated Heap Size: Size: 0x1a800 (108544) bytes. GC Committed Heap Size: Size: 0x22000 (139264) bytes.你應(yīng)該沒忘記我們對象的地址吧?
莫妮的地址是?000002470000c0c8,而新生段的分配信息我們也可以清晰的看到。
當然有關(guān)段,估計仍然需要一大章節(jié)才能說明白吧,這里僅僅簡單介紹下。
它是GC從操作系統(tǒng)采集內(nèi)存的一個單位,實際內(nèi)存申請和分配以及釋放以segment(段)為單位;
例如:?workstation GC模式segment大小為16M,server GC模式segment大小為64M。
Gen 0和Gen 1?heap總是位于同一個段中,叫做ephemeral segment(新生段),
Gen 2?heap由0個或多個segments組成,LOH由1個或多個segments組成
.NET程序啟動時CLR為heap創(chuàng)建2個segment,一個作為ephemeral segment,另一個用于LOH。
Full GC后完全空閑的segments將被釋放掉,內(nèi)存返回給操作系統(tǒng)
再次深入前,讓我們來點小甜點,放松一下,看看四周的風(fēng)景。
4.1 我們怎么用DRAM
不管怎么分配,我們都需要涉及到物理內(nèi)存。
當然,我們并不支持使用物理內(nèi)存!
我們使用虛擬內(nèi)存(VM),這塊有操作系統(tǒng)的哦VMM(虛擬內(nèi)存管理器)提供。
操作系統(tǒng)引入了虛擬內(nèi)存概念,使得我們能夠:
每個進程都認為它有自己的內(nèi)存空間,就好象國家的廉租房制度一樣,讓每個人都體驗到家的溫馨。
你可以請求更多的內(nèi)存,甚至超過了物理內(nèi)存大小,而管理器只會占用真正使用的物理內(nèi)存;
重要的是,不需要VM分配為連續(xù)的了,實現(xiàn)了即拋即用。
VM的實現(xiàn)也很有意思,由操作系統(tǒng)提供頁的支持:
由MMU(內(nèi)存管理單元)實現(xiàn)
內(nèi)存被分割為頁(一般是4K)
虛擬內(nèi)存到物理內(nèi)存由頁映射使用頁表進行管理
無法映射到物理內(nèi)存,會導(dǎo)致頁失敗錯誤
操作系統(tǒng)控制頁映射轉(zhuǎn)換
有很多技術(shù)實現(xiàn)更快的轉(zhuǎn)換,比如頁表緩存、TLB(Translation Lookaside Buffer)技術(shù)等。
4.2 物理頁是怎么組織的?
當計算機啟動后,Windows操作系統(tǒng)把來自DRAM的物理頁整理為一個列表;
當有進程需要物理頁分配時,它轉(zhuǎn)變?yōu)閃S(Working Set)的一部分
當一個物理頁從WS移除后,它通過軟件頁故障或硬頁故障返回到列表
硬頁故障是非常耗時的,因此我們需要避免它
5.為了避免硬頁故障,我們不能增加大于物理內(nèi)存的堆棧(可以觀察物理內(nèi)存負載信息)
4.3 GC怎么從VM采集內(nèi)存
保留內(nèi)存
由于需要分頁的原因,因此我們可以請求稍后可能使用的范圍地址,它被稱作保留內(nèi)存(VirtualAlloc 使用 MEM_RESERVE)。當然保留內(nèi)存不能保存任何數(shù)據(jù)。
提交內(nèi)存
當我們需要在頁存儲上存儲數(shù)據(jù)時,我們告訴操作系統(tǒng),這叫提交內(nèi)存。(VirtualAlloc使用MEM_COMMIT),提交操作成功后,保證你不會得到OOM異常。
保留內(nèi)存操作是非常快的,當然你仍然需要增加一次用戶態(tài)<–> 內(nèi)核態(tài)的操作;提交內(nèi)存也是非常快的… 當然,知道你真正的保存數(shù)據(jù)。而恰恰這個時候,有可能引起分配頁故障,導(dǎo)致OOM。
保存數(shù)據(jù)
一切都oK了,我們呢就可以輕松保存數(shù)據(jù)了。
5. 再次深入內(nèi)存布局調(diào)查
讓我們回到從前,一如第一次初見。
5.1 初見
假設(shè)上圖就是我們的段(segment)內(nèi)存的保留內(nèi)存(Reserve memory)區(qū)域,那么你想到了什么?
是的,首先她是一個空蕩蕩的巨大空間!
當然,這里面也沒有任何東西。
5.2 相識
現(xiàn)在,我們想要在段內(nèi)存中保存一些東東,該怎么辦呢?
是的,我們得混個臉熟!
好了,首先我們需要保存段的頭信息,那讓我們先提交個申請(通常是64K)。
有了第一次后,我們對這個操作流程應(yīng)該熟門熟路了,所以,誰也抵擋不住我們前進的腳步。
再次提交存儲對象的空間請求(通常是64K),當然,GC通常不會僅僅為一個對象申請內(nèi)存.
5.3 行動
它通常先申請一個分配上下文,當然這個時候并沒有對象被構(gòu)造。
然后動用物理內(nèi)存頁,保存數(shù)據(jù),查看存儲信息如下:
0:007> dq 000002470000c0c8-8 l3 00000247`0000c0c0 00000000`00000000 00007ffc`77c13798 00000247`0000c0d0 00000100`00000080其內(nèi)部大致的流程如下(精簡版):
注意:緩存是非常快的,以下是來自Intel的數(shù)據(jù)。
L1 緩存:4 cpu周期
L2 緩存:12 cpu周期
L3 緩存:44 cpu周期
DRAM的讀取大約 60ns ~ 100ns之間。
5.4 小結(jié)下
經(jīng)過前面不斷的深入探索,對象的內(nèi)存分布已經(jīng)在你面前完全展開。那么,讓我們再總結(jié)下。
GC的分配如下:
6. 清掃戰(zhàn)場
經(jīng)過上面讓人目眩神秘的命令和圖片,你學(xué)廢了嗎?
最后,讓我們打掃下戰(zhàn)場,看看GC這位小寶貝。
6.1 GC怎么決定收集
如下代碼,讓我們看看它能有多智能?
public static int Main() {MaoniType o = new MaoniType(128, 256);GCHandle h = GCHandle.Alloc(o, GCHandleType.Weak);GC.Collect();Console.WriteLine("Collect called, h.Target is {0}",(h.Target == null) ? "collected" : "not collected");return 0; }發(fā)生了什么?輸出是:
Output - Collect called, h.Target is not collected
是的,你沒有看錯,GC.Collect()收集整個堆棧,這意味著GC不能決定對象的生命周期。
如果一個對象還活著,那么GC會被告知,在這個例子中,JIT(User Roots)告訴GC,對象還活著。因此GC無法回收對象。
6.2 開始收集
好了,讓我們來個真正的回收。
[MethodImpl(MethodImplOptions.NoInlining)]public static void TestLifeTime(){MaoniType o = new MaoniType(128, 256);h = GCHandle.Alloc(o, GCHandleType.Weak);}public static int Main(){TestLifeTime();GC.Collect();Console.WriteLine("Collect called, h.Target is {0}",(h.Target == null) ? "collected" : "not collected");return 0;}輸出結(jié)果:
Output: Collect called, h.Target is collected
再次觀察GC:
是的,GC摧毀了對象,內(nèi)存回收了。
7. 小結(jié)
經(jīng)過本次的多次深入刨析,你對你的對象是不是更加了解了?
👓都看到這了,還在乎點個贊嗎?
👓都點贊了,還在乎一個收藏嗎?
👓都收藏了,還在乎一個評論嗎?
總結(jié)
以上是生活随笔為你收集整理的秘境探索之一个.NET 对象从内存分配到内存回收的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 利用SOS扩展库进入高阶.NET6程序的
- 下一篇: Exceptionless服务端+kib