C# CLR 聊聊對象的內存佈局 一個空對象佔用多少內存

在 C# 中的對象大概能夠分爲三個不一樣的類型,包括值類型、引用類型和其餘類型。本文主要討論的是引用類型對內存空間的佔用狀況。在討論開始以前我想問問你們,一個空的對象會佔用多少內存空間?固然這個問題自己就有問題,由於沒有區分棧空間與堆空間的內存空間。其實小夥伴會發現這不是一個好回答的問題,由於彷佛沒有一個能夠認爲標準的標準答案。請讓我爲你詳細聊聊對象的內存佈局php

在開始以前,先廣告一下農夫的書 《.NET Core底層入門》 這本書寫的很是底層,內存這一篇章寫的特別棒。若是看本文以後以爲更加迷糊了,請看農夫大大的書git

開始的問題其實問題自己不算對,爲何呢?由於咱 .NET 能夠在 x86 和 x64 和 ARM 等運行,而運行時包括了 .NET Framework 和 .NET Core 還有 mono 和 .NET Micro Framework 等,這些表現有稍微的不一樣。至少有趣的 .NET Framework 有超級多個不一樣的版本,本考古學家也不能肯定這些版本之間是否存在差別,只是聽小夥伴吹過。而 .NET Micro Framework (NETMF) 自己就是設計在極度小的內存下運行,裏面對引用類型作了不少有趣的優化,而我僅僅知道有優化,具體作了什麼就不知道了,也不想知道github

而 .NET Core 下還有一個有趣的技術叫 .NET Native 經過這個有趣的技術能夠極大混淆引用類型和值類型的概念,這個技術底層沒啥文檔,須要本身去翻代碼。是否有差異還請大佬們教教我數據結構

本文僅能告訴你們的只有是 .NET Core 3.1 在 x86 和 x64 下的引用類型的內存佔用狀況佈局

在我寫本文的時候,其實是很慌的,有太多的分支我沒有理清楚。在從新閱讀了農夫的 《.NET Core底層入門》和 《CLR via C#》和 https://github.com/dotnet/runtime 的很小一部分代碼以後,稍微有點底氣來和你們聊聊性能

如下狀況是不在本文討論範圍測試

  • .NET Framework
  • .NET Micro Framework
  • Mono
  • IL2CPP
  • .NET Native
  • WASM
  • ARM
  • ARM64
  • AOT
  • Itanium

在說到內存優化等,這裏說的內存默認都是說堆空間的內存空間。爲何不提到棧空間的內存空間?由於棧空間默認是固定大小(.NET Core)也就是用或不用都須要這麼大的空間。而棧空間會隨方法的執行結束自動清空方法佔用的棧空間,這部分就包含了局部變量佔用的棧空間。所以使用棧空間不存在內存回收壓力,也不存在內存分配的性能問題。但棧空間是很小的一段空間,一旦用完將會拋出堆棧溢出優化

所以本文所說的空對象佔用的內存空間僅說佔用的堆空間的內存空間,這不意味着本文說的對象僅僅是引用類型對象,此時值類型對象也是能包含的。但惋惜的是我不許備直接討論值類型對象在堆空間的狀況ui

在開始以前,請讓咱忽略吃雞蛋應該從大的一頭開始吃仍是從小的一頭開始吃的問題,從 x86 和 x64 開始比較好,這是從雞蛋小的一頭開始吃的故事。等等,怎麼到了吃雞蛋的時候了?其實我說的是大端和小端的問題哈。在 .NET Core 下,在 x86 與 x86-64 平臺儲存整數使用的是 Little Endian 小端法,而在 ARM 與 ARM64 平臺儲存整數使用的是 Big Endian 大端法。具體這兩個存儲方法有啥不一樣,請自行搜尋或看農夫的《.NET Core底層入門》 的第7章第二節.net

試試在 VS 裏面新建一個控制檯程序,在裏面建立一個對象,看看他的內存佈局是如何的

static void Main(string[] args)
        {
            var obj = new object();
        }

在 obj 建立完成的下一行添加斷點,運行此斷點則內存中存在建立完成的 obj 對象

那如何在 VS 裏面查看某個對象的內存?點擊調試窗口內存,在內存窗口裏面,能夠打開4個不一樣的內存窗口,同時看4個不一樣的內存。默認打開內存1窗口就足夠了。這裏的內存4個窗口只是提供了4個窗口能夠查看不一樣的內容,能看到的內存是相同的內存

在內存裏面查看某個對象的內存的方法是輸入這個對象的變量名

按下回車以後將會自動將變量名修改這個變量對象的內存的地址

這個表明什麼意思呢?儘管能夠看到內存裏面的值,可是依然須要一點文檔的輔助,才能瞭解含義。按照程序運行的原理,內存的值若是脫離了數據結構,那麼將沒有任意意義,和亂碼是相同的。可是有了對應的數據結構,那麼將能夠解析出裏面的含義

從農夫的《.NET Core底層入門》書中能夠看到,引用類型對象的值由如下三個部分組成

  • 對象頭 (Object Header)
  • 類型信息 (MethodTable Pointer)
  • 各個字段的內容

對象頭包含標誌與同步塊索引 (SyncBlock Index) 等數據,在 32 位平臺上佔用 4 個字節,在 64 位平臺上佔用 8 個字節但只有後 4 個字節會使用。類型信息是一個內存地址,指向 .NET 運行時內部保存的類型數據 (類型是 MethodTable),在 32 位平臺上佔用 4 個字節,在 64 位平臺上佔用 8 個字節

而默認運行的控制檯是使用 AnyCpu 執行的,而個人系統是 x64 系統,換句話說,此時的 .NET 程序是 x64 程序。在 x64 程序中,根據上面描述能夠知道,類型信息佔用了 8 個字節

又根據 .NET 中引用類型對象自己儲存的內存地址指向類型信息的開始,而對象頭會在 對象內存地址 - 4 的位置,能夠了解到,當前內存裏面顯示的內容只是類型信息 (MethodTable Pointer) 的值

由於咱建立的是一個空的 object 對象,所以不包含任何字段,能夠看到的內容以下

0x00000231B98AAD70  e8 0a 2e 5c fc 7f 00 00  ?..\?...
0x00000231B98AAD78  00 00 00 00 00 00 00 00  ........
0x00000231B98AAD80  00 00 00 00 00 00 00 00  ........
0x00000231B98AAD88  00 00 00 00 00 00 00 00  ........

而對象頭開始的地方是在 對象內存地址 - 4 的地址,能夠在內存地址欄添加上 -4 以下圖所示看到對象頭的值

爲何在 對象內存地址 - 4 的地址就是對象頭的值?在 x64 和 x86 是相同的?沒錯,如上面所說,儘管對象頭會在 x64 佔用 8 個字節,可是隻有後 4 個字節會使用,所以 -4 就能看到對象頭了

那麼如何校驗一下關於對象頭和類型信息的值,拿到的是對的值?能夠在控制檯裏面多建立幾個空對象,根據相同類型的對象的類型信息必定相同的原理,能夠判斷咱剛纔拿到的類型信息是不是對的。若是多個相同的 object 的類型信息都是相同的值,那麼證實多個相同的 object 類型的對象使用了指向相同的內存空間的類型信息

static void Main(string[] args)
        {
            var obj = new object();
            var obj1 = new object();
            var obj2 = new object();
            var obj3 = new object();
        }

如今建立了 4 個 object 對象了,在執行代碼的最後一句以後添加斷點,而後運行

理論上此時的應用程序將會將這幾個對象作連續的分配,由於此時的堆空間尚未內容

咱先輸入 obj 到內存窗口的地址欄,我能夠看到如下信息

0x000002532039AD70  e8 0a 96 60 fc 7f 00 00  ?.?`?...
0x000002532039AD78  00 00 00 00 00 00 00 00  ........
0x000002532039AD80  00 00 00 00 00 00 00 00  ........
0x000002532039AD88  e8 0a 96 60 fc 7f 00 00  ?.?`?...
0x000002532039AD90  00 00 00 00 00 00 00 00  ........
0x000002532039AD98  00 00 00 00 00 00 00 00  ........
0x000002532039ADA0  e8 0a 96 60 fc 7f 00 00  ?.?`?...
0x000002532039ADA8  00 00 00 00 00 00 00 00  ........
0x000002532039ADB0  00 00 00 00 00 00 00 00  ........
0x000002532039ADB8  e8 0a 96 60 fc 7f 00 00  ?.?`?...
0x000002532039ADC0  00 00 00 00 00 00 00 00  ........
0x000002532039ADC8  00 00 00 00 00 00 00 00  ........

能夠看到有相同的 e8 0a 96 60 fc 7f 00 00 這一段二進制值,那麼這應該就是類型信息所在的地址了。嘗試看一下這個地址的值

根據在 x86 和 x64 下是小端顯示的,也就是 e8 0a 96 60 fc 7f 00 00 須要按照字節反過來寫纔是十六進制的值,反過來寫是 0x00007ffc60960ae8 至關於去掉空格,而後兩個字符兩個字符,從後到前寫一次

打開另外一個內存窗口,輸入 0x00007ffc60960ae8 到地址欄,就能夠看到 類型信息 的內存值

我這裏截取一部份內容放在下面,用於證實這就是 類型信息 的內存值

0x00007FFC60960AE8  00 02 00 00 18 00 00 00  ........
0x00007FFC60960AF0  08 40 7d 00 04 00 00 00  .@}.....
0x00007FFC60960AF8  00 00 00 00 00 00 00 00  ........
0x00007FFC60960B00  20 40 86 60 fc 7f 00 00   @?`?...
0x00007FFC60960B08  50 0b 96 60 fc 7f 00 00  P.?`?...
0x00007FFC60960B10  18 bb 98 60 fc 7f 00 00  .??`?...
0x00007FFC60960B18  88 00 99 60 fc 7f 00 00  ?.?`?...
0x00007FFC60960B20  00 00 00 00 00 00 00 00  ........

如何證實這就是 類型信息 的內存值?其實嘗試屢次執行控制檯,看看每次 obj 對應的 類型信息指針 指向的內存地址的值是否是和當前的相同,若是相同,那麼證實這就是 類型信息 的值了

如上述測試,咱能夠了解到在 x64 下一個 object 空對象在內存中佔用的 byte 數量是 3 * 8 個字節大小

  • 8 字節表示對象頭
  • 8 字節表示類型信息的內存地址的值
  • 8 字節用於 object 的佔坑信息(字段內存對齊)

上面是否是歪樓了?什麼是佔坑信息?其實就是原本放字段的空間。咱試試在某個類裏面方一個簡單的 int 在裏面填寫特殊的數值,用來找到內存的存放這個字段的空間

class Program
    {
        static void Main(string[] args)
        {
            var obj = new object();
            var obj1 = new object();
            var obj2 = new object();
            var obj3 = new object();

            var p1 = new Program();
        }

        private uint _foo = 0xFF020306;
    }

如上面代碼在 Program 類添加 _foo 字段,而後建立出這個對象,理論上此時這個對象應該是在全部 object 對象的後面。注意,在內存裏面有不多對象的時候確實能夠這麼說,後建立的對象就恰好放在新建立的對象後面。若是內存裏面存在碎片的時候,上面這句話就不必定對了。不過咱的測試程序足夠簡單,所以這句話仍是對的

咱繼續在內存地址欄輸入 obj 按下回車,此時顯示的內存就是這幾個對象的內存

0x000002961B1CAD70  e8 0a 94 60 fc 7f 00 00  ?.?`?...
0x000002961B1CAD78  00 00 00 00 00 00 00 00  ........
0x000002961B1CAD80  00 00 00 00 00 00 00 00  ........
0x000002961B1CAD88  e8 0a 94 60 fc 7f 00 00  ?.?`?...
0x000002961B1CAD90  00 00 00 00 00 00 00 00  ........
0x000002961B1CAD98  00 00 00 00 00 00 00 00  ........
0x000002961B1CADA0  e8 0a 94 60 fc 7f 00 00  ?.?`?...
0x000002961B1CADA8  00 00 00 00 00 00 00 00  ........
0x000002961B1CADB0  00 00 00 00 00 00 00 00  ........
0x000002961B1CADB8  e8 0a 94 60 fc 7f 00 00  ?.?`?...
0x000002961B1CADC0  00 00 00 00 00 00 00 00  ........
0x000002961B1CADC8  00 00 00 00 00 00 00 00  ........
0x000002961B1CADD0  88 1b a2 60 fc 7f 00 00  ?.?`?...
0x000002961B1CADD8  06 03 02 ff 00 00 00 00  ........

請先注意第一行,能夠看到此時的類型信息的內存地址的值和以前一次運行的不相同了,此次的值是 e8 0a 94 60 fc 7f 00 00 咱先嚐試在另外一個內存窗口輸入這個地址 0x00007ffc60940ae8 看看類型信息的內存

大概截取一下內容

0x00007FFC60940AE8  00 02 00 00 18 00 00 00  ........
0x00007FFC60940AF0  08 40 7d 00 04 00 00 00  .@}.....
0x00007FFC60940AF8  00 00 00 00 00 00 00 00  ........
0x00007FFC60940B00  20 40 84 60 fc 7f 00 00   @?`?...
0x00007FFC60940B08  50 0b 94 60 fc 7f 00 00  P.?`?...
0x00007FFC60940B10  18 bb 96 60 fc 7f 00 00  .??`?...
0x00007FFC60940B18  88 00 97 60 fc 7f 00 00  ?.?`?...
0x00007FFC60940B20  00 00 00 00 00 00 00 00  ........

能夠和上面的值對比一下,大部分都是相同的,而後依然有幾個歪樓的值,咱這裏就先忽略

好接下來找到剛纔定義的 _foo 的值,咱給他的是 0xFF020306 而根據小端的寫法,將會是以下的值 06 03 02 ff 沒錯,恰好放在了最後一行裏面

0x000002961B1CADD8  06 03 02 ff 00 00 00 00  ........

複習一下,在 C# 裏面不管在 x86 仍是 x64 下,每一個 int 都佔領 4 個字節

若是以爲不夠直觀,咱修改一下對象建立的順序,請看代碼

static void Main(string[] args)
        {
            var obj = new object();
            var p1 = new Program();
            var obj1 = new object();
            var obj2 = new object();
            var obj3 = new object();
        }

此時在內存窗口輸入 obj 按下回車能夠看到的值以下

0x00000106BFF2AD68  00 00 00 00 00 00 00 00  ........
0x00000106BFF2AD70  e8 0a 96 60 fc 7f 00 00  ?.?`?...
0x00000106BFF2AD78  00 00 00 00 00 00 00 00  ........
0x00000106BFF2AD80  00 00 00 00 00 00 00 00  ........
0x00000106BFF2AD88  88 1b a4 60 fc 7f 00 00  ?.?`?...
0x00000106BFF2AD90  06 03 02 ff 00 00 00 00  ........
0x00000106BFF2AD98  00 00 00 00 00 00 00 00  ........
0x00000106BFF2ADA0  e8 0a 96 60 fc 7f 00 00  ?.?`?...
0x00000106BFF2ADA8  00 00 00 00 00 00 00 00  ........
0x00000106BFF2ADB0  00 00 00 00 00 00 00 00  ........
0x00000106BFF2ADB8  e8 0a 96 60 fc 7f 00 00  ?.?`?...
0x00000106BFF2ADC0  00 00 00 00 00 00 00 00  ........
0x00000106BFF2ADC8  00 00 00 00 00 00 00 00  ........
0x00000106BFF2ADD0  e8 0a 96 60 fc 7f 00 00  ?.?`?...
0x00000106BFF2ADD8  00 00 00 00 00 00 00 00  ........

儘管 _foo 是一個int只佔用了 4 個字節,可是根據字節對齊,後面的 4 個字節依然空閒不用。我也就是將他算在了這個對象上面

看到這裏小夥伴是否是可以大概知道爲何這個問題很差回答了,一個空的對象一定佔的內存必定包括 對象頭(syncblk信息)和類型信息,然後面的字段的空間就有點爭議了,由於不肯定是否要將佔坑的加上去。儘管這個空間不是我這個對象用的,可是其餘對象也不用這部分空間

以上是 x64 下的對象內存佈局,大概能夠認定答案是一個空對象佔用了3*8個字節

那麼 x86 下的對象會如何?修改一下配置,讓控制檯在 x86 下執行

根據農夫大大的書能夠了解在 x86 下的對象頭和類型信息都是佔 4 個字節。而此時對象的佔坑的字段也是 4 個字節,所以一個對象佔用的內存是 3*4 個字節

運行剛纔的程序,繼續在內存窗口輸入 obj 按下回車,此時能夠看到的內存信息以下圖。固然你看到的值應該和我看到的不相同

0x057EA794  9c 00 64 05 00 00 00 00  ?.d.....
0x057EA79C  00 00 00 00 e8 eb df 07  ....???.
0x057EA7A4  06 03 02 ff 00 00 00 00  ........
0x057EA7AC  9c 00 64 05 00 00 00 00  ?.d.....
0x057EA7B4  00 00 00 00 9c 00 64 05  ....?.d.
0x057EA7BC  00 00 00 00 00 00 00 00  ........
0x057EA7C4  9c 00 64 05 00 00 00 00  ?.d.....
0x057EA7CC  00 00 00 00 00 00 00 00  ........

這裏的 9c 00 64 05 就是 Object 的類型信息,然後面的 00 00 00 00 就是佔坑的字段空間。第一行是由於 obj 指向的內存是對象的類型信息,而對象的對象頭信息是放在類型信息前面,所以在上圖就沒有看到第一個對象的對象頭

大概看到這裏,相信小夥伴也能理解一個空對象在佔用了多少堆內存空間了

那麼是否是有小夥伴好奇空對象能夠在棧空間佔用多少內存?回答是0到爆棧這麼大,看你如何用

我搭建了本身的博客 https://blog.lindexi.com/ 歡迎你們訪問,裏面有不少新的博客。只有在我看到博客寫成熟以後纔會放在csdn或博客園,可是一旦發佈了就再也不更新

若是在博客看到有任何不懂的,歡迎交流,我搭建了 dotnet 職業技術學院 歡迎你們加入

若有不方便在博客評論的問題,能夠加我 QQ 2844808902 交流

知識共享許可協議
本做品採用知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議進行許可。歡迎轉載、使用、從新發布,但務必保留文章署名林德熙(包含連接:http://blog.csdn.net/lindexi_gd ),不得用於商業目的,基於本文修改後的做品務必以相同的許可發佈。若有任何疑問,請與我聯繫

相關文章
相關標籤/搜索