.NET中的數(shù)組在內(nèi)存中如何布局?
當(dāng)前位置:點(diǎn)晴教程→知識(shí)管理交流
→『 技術(shù)文檔交流 』
總的來(lái)說(shuō),.NET的值類型和引用類型都映射一段連續(xù)的內(nèi)存片段。不過(guò)對(duì)于值類型對(duì)象來(lái)說(shuō),這段內(nèi)存只需要存儲(chǔ)其字段成員,而對(duì)應(yīng)引用類型對(duì)象,還需要存儲(chǔ)額外的內(nèi)容。就內(nèi)存布局來(lái)說(shuō),引用類型有兩個(gè)獨(dú)特的存在,一個(gè)是字符串,另一個(gè)就是數(shù)組。我在《你知道.NET的字符串在內(nèi)存中是如何存儲(chǔ)的嗎?》一文中對(duì)字符串的內(nèi)存布局作了詳細(xì)介紹,今天我們來(lái)聊聊數(shù)組類型的內(nèi)存布局。
一、引用類型布局但是對(duì)于引用類型對(duì)象,除了存儲(chǔ)其所有字段成員外,還需要存儲(chǔ)一個(gè)Object Header和TypeHandle,前者可以用來(lái)存儲(chǔ)Hash值,也可以用來(lái)存儲(chǔ)同步狀態(tài);后者存儲(chǔ)的是目標(biāo)類型方法表的地址(詳細(xì)介紹可以參考我的文章《如何計(jì)算一個(gè)實(shí)例占用多少內(nèi)存?》、《如何將一個(gè)實(shí)例的內(nèi)存二進(jìn)制內(nèi)容讀出來(lái)?》。 如下圖所示,對(duì)于32位(x86)系統(tǒng),Object Header和TypeHandle各占據(jù)4個(gè)字節(jié);但是對(duì)于64位(x64)來(lái)說(shuō),存儲(chǔ)方法表指針的TypeHandle自然擴(kuò)展到8個(gè)字節(jié),但是Object Header依然是4個(gè)字節(jié),為了確保TypeHandle基于8字節(jié)的內(nèi)存對(duì)齊,所以會(huì)前置4個(gè)字節(jié)的“留白(Padding)”。 順便說(shuō)一下,即使沒有定義任何的字段成員,運(yùn)行時(shí)依然會(huì)使用一個(gè)“指針寬度(IntPtr.Size)”的存儲(chǔ)空間(上圖中的Payload),所以x86/x64系統(tǒng)中一個(gè)引用類型對(duì)象至少占據(jù)12/24字節(jié)的內(nèi)存。除此之外,所謂對(duì)象的引用并不是指向這段內(nèi)存的起始位置,而是指向TypeHandle的地址。 二、數(shù)組類型布局既然數(shù)組是引用類型,它自然按照上面的方式進(jìn)行內(nèi)存布局。它依然擁有4字節(jié)的Object Header,TypeHandle部分存儲(chǔ)的是數(shù)組類型自身的方法表地址。其荷載內(nèi)容(Payload)采用如下的布局:前置4個(gè)字節(jié)以UInt32的形式存儲(chǔ)數(shù)組的長(zhǎng)度,后面依次存儲(chǔ)每個(gè)數(shù)組元素的內(nèi)容。對(duì)于64位(x64)來(lái)說(shuō),為了確保數(shù)組元素的內(nèi)存對(duì)齊,兩者之間具有4個(gè)字節(jié)的Padding。 三、值類型數(shù)組對(duì)于值類型的數(shù)組,Payload部分直接存儲(chǔ)元素自身的值。如下程序演示了如何將一個(gè)字節(jié)數(shù)組對(duì)象在內(nèi)存中的字節(jié)序列讀出來(lái)。如代碼片段所示,GetArray方法根據(jù)上述的內(nèi)存布局計(jì)算出一個(gè)數(shù)組對(duì)象占據(jù)的字節(jié)數(shù),并創(chuàng)建出對(duì)應(yīng)的字節(jié)數(shù)據(jù)來(lái)存儲(chǔ)數(shù)組對(duì)象的字節(jié)內(nèi)容。我們?cè)谏厦嬲f(shuō)過(guò),一個(gè)數(shù)組變量指向的是目標(biāo)對(duì)象TypeHandle部分的地址,所以我們需要前移一個(gè)指針寬度才能得到內(nèi)存的起始位置。我們最終利用起始位置和字節(jié)數(shù),將承載數(shù)組自身對(duì)象的字節(jié)讀出來(lái)存放到預(yù)先創(chuàng)建的字節(jié)數(shù)組中。 var array = new byte[] { byte.MaxValue, byte.MaxValue, byte.MaxValue }; Console.WriteLine($"Array: {BitConverter.ToString(GetArray(array))}"); Console.WriteLine($"TypeHandle of Byte[]: {BitConverter.ToString(GetTypeHandle<byte[]>())}");unsafe static byte[] GetArray<T>(T[] array) { var size = IntPtr.Size // Object header + Padding + IntPtr.Size // TypeHandle + IntPtr.Size // Length + Padding + Unsafe.SizeOf<T>() * array.Length // Elements ; var bytes = new byte[size]; var pointer = Unsafe.AsPointer(ref array); var head = *(IntPtr*)pointer - IntPtr.Size; Marshal.Copy(head, bytes, 0, size); return bytes; } unsafe static byte[] GetTypeHandle<T>() => BitConverter.GetBytes(typeof(T).TypeHandle.Value); 為了進(jìn)一步驗(yàn)證數(shù)組對(duì)象每個(gè)部分的內(nèi)容,我們還定義了GetTypeHandle<T>方法讀取目標(biāo)類型TypeHandle的值(方法表地址)。在演示程序中,我們創(chuàng)建了一個(gè)長(zhǎng)度位3的字節(jié)數(shù)組,并將三個(gè)數(shù)組元素的值設(shè)置位byte.MaxValue。我們將承載這個(gè)數(shù)組的字節(jié)序列和字節(jié)數(shù)組類型的TypeHandle的值打印出來(lái)。 Array: [00-00-00-00-00-00-00-00]-[E0-6A-0D-01-FF-7F-00-00]-[03-00-00-00]-00-00-00-00-[FF-FF-FF] TypeHandle of Byte[]: E0-6A-0D-01-FF-7F-00-00 如上所示的輸出結(jié)果驗(yàn)證了數(shù)組對(duì)象的內(nèi)存布局。由于演示機(jī)器為64位系統(tǒng),所以前8個(gè)字節(jié)表示Object Header(4字節(jié))和Padding(4字節(jié))。中間高亮的8個(gè)字節(jié)正好與字節(jié)數(shù)組類型的TypeHandle的值一致。后面4個(gè)字節(jié)(03-00-00-00)表示字節(jié)的長(zhǎng)度(3),緊隨其后的4個(gè)字節(jié)位Padding。最后的內(nèi)容正好是三個(gè)數(shù)組元素的值(FF-FF-FF)。 四、引用類型數(shù)組對(duì)于引用類型的數(shù)組,其每個(gè)數(shù)組元素存儲(chǔ)是元素對(duì)象的地址,下面的程序驗(yàn)證了這一點(diǎn)。如代碼片段所示,我們定義了GetAddress<T>方法得到指定變量指向的目標(biāo)地址,并將其轉(zhuǎn)換成返回的字節(jié)數(shù)組。演示程序創(chuàng)建了一個(gè)包含三個(gè)元素的字符串?dāng)?shù)組,我們將承載數(shù)組對(duì)象的字節(jié)序列和作為數(shù)組元素的三個(gè)字符串對(duì)象的地址打印出來(lái)。 var s1 = "foo"; var s2 = "bar"; var s3 = "baz"; var array = new string[] { s1, s2, s3 }; Console.WriteLine($"Array: {BitConverter.ToString(GetArray(array))}"); Console.WriteLine($"element 1: {BitConverter.ToString(GetAddress(ref s1))}"); Console.WriteLine($"element 2: {BitConverter.ToString(GetAddress(ref s2))}"); Console.WriteLine($"element 3: {BitConverter.ToString(GetAddress(ref s3))}");unsafe static byte[] GetAddress<T>(ref T value) { var address = *(IntPtr*)Unsafe.AsPointer(ref value); return BitConverter.GetBytes(address); } 從如下的代碼片段可以看出,在承載數(shù)組對(duì)象的字節(jié)序列中,最后的24字節(jié)正好是三個(gè)字符串的地址。 Array: 00-00-00-00-00-00-00-00-48-E9-5E-03-FF-7F-00-00-03-00-00-00-00-00-00-00-E0-EF-40-73-72-02-00-00-00-F0-40-73-72-02-00-00-20-F0-40-73-72-02-00-00 element 1: E0-EF-40-73-72-02-00-00 element 2: 00-F0-40-73-72-02-00-00 element 3: 20-F0-40-73-72-02-00-00 該文章在 2023/11/27 11:15:11 編輯過(guò) |
關(guān)鍵字查詢
相關(guān)文章
正在查詢... |