.NET中的數組在內存中如何布局?
當前位置:點晴教程→知識管理交流
→『 技術文檔交流 』
總的來說,.NET的值類型和引用類型都映射一段連續的內存片段。不過對于值類型對象來說,這段內存只需要存儲其字段成員,而對應引用類型對象,還需要存儲額外的內容。就內存布局來說,引用類型有兩個獨特的存在,一個是字符串,另一個就是數組。我在《你知道.NET的字符串在內存中是如何存儲的嗎?》一文中對字符串的內存布局作了詳細介紹,今天我們來聊聊數組類型的內存布局。
一、引用類型布局但是對于引用類型對象,除了存儲其所有字段成員外,還需要存儲一個Object Header和TypeHandle,前者可以用來存儲Hash值,也可以用來存儲同步狀態;后者存儲的是目標類型方法表的地址(詳細介紹可以參考我的文章《如何計算一個實例占用多少內存?》、《如何將一個實例的內存二進制內容讀出來?》。 如下圖所示,對于32位(x86)系統,Object Header和TypeHandle各占據4個字節;但是對于64位(x64)來說,存儲方法表指針的TypeHandle自然擴展到8個字節,但是Object Header依然是4個字節,為了確保TypeHandle基于8字節的內存對齊,所以會前置4個字節的“留白(Padding)”。 順便說一下,即使沒有定義任何的字段成員,運行時依然會使用一個“指針寬度(IntPtr.Size)”的存儲空間(上圖中的Payload),所以x86/x64系統中一個引用類型對象至少占據12/24字節的內存。除此之外,所謂對象的引用并不是指向這段內存的起始位置,而是指向TypeHandle的地址。 二、數組類型布局既然數組是引用類型,它自然按照上面的方式進行內存布局。它依然擁有4字節的Object Header,TypeHandle部分存儲的是數組類型自身的方法表地址。其荷載內容(Payload)采用如下的布局:前置4個字節以UInt32的形式存儲數組的長度,后面依次存儲每個數組元素的內容。對于64位(x64)來說,為了確保數組元素的內存對齊,兩者之間具有4個字節的Padding。 三、值類型數組對于值類型的數組,Payload部分直接存儲元素自身的值。如下程序演示了如何將一個字節數組對象在內存中的字節序列讀出來。如代碼片段所示,GetArray方法根據上述的內存布局計算出一個數組對象占據的字節數,并創建出對應的字節數據來存儲數組對象的字節內容。我們在上面說過,一個數組變量指向的是目標對象TypeHandle部分的地址,所以我們需要前移一個指針寬度才能得到內存的起始位置。我們最終利用起始位置和字節數,將承載數組自身對象的字節讀出來存放到預先創建的字節數組中。 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); 為了進一步驗證數組對象每個部分的內容,我們還定義了GetTypeHandle<T>方法讀取目標類型TypeHandle的值(方法表地址)。在演示程序中,我們創建了一個長度位3的字節數組,并將三個數組元素的值設置位byte.MaxValue。我們將承載這個數組的字節序列和字節數組類型的TypeHandle的值打印出來。 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 如上所示的輸出結果驗證了數組對象的內存布局。由于演示機器為64位系統,所以前8個字節表示Object Header(4字節)和Padding(4字節)。中間高亮的8個字節正好與字節數組類型的TypeHandle的值一致。后面4個字節(03-00-00-00)表示字節的長度(3),緊隨其后的4個字節位Padding。最后的內容正好是三個數組元素的值(FF-FF-FF)。 四、引用類型數組對于引用類型的數組,其每個數組元素存儲是元素對象的地址,下面的程序驗證了這一點。如代碼片段所示,我們定義了GetAddress<T>方法得到指定變量指向的目標地址,并將其轉換成返回的字節數組。演示程序創建了一個包含三個元素的字符串數組,我們將承載數組對象的字節序列和作為數組元素的三個字符串對象的地址打印出來。 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); } 從如下的代碼片段可以看出,在承載數組對象的字節序列中,最后的24字節正好是三個字符串的地址。 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 編輯過 |
關鍵字查詢
相關文章
正在查詢... |