在 C# 中的对象大概可以分为三个不同的类型,包括值类型、引用类型和其他类型。本文主要讨论的是引用类型对内存空间的占用情况。在讨论开始之前我想问问大家,一个空的对象会占用多少内存空间?当然这个问题本身就有问题,因为没有区分栈空间与堆空间的内存空间。其实小伙伴会发现这不是一个好回答的问题,因为似乎没有一个可以认为标准的标准答案。请让我为你详细聊聊对象的内存布局

在开始之前,先广告一下农夫的书 《.NET Core底层入门》 这本书写的非常底层,内存这一篇章写的特别棒。如果看本文之后觉得更加迷糊了,请看农夫大大的书

开始的问题其实问题本身不算对,为什么呢?因为咱 .NET 可以在 x86 和 x64 和 ARM 等运行,而运行时包括了 .NET Framework 和 .NET Core 还有 mono 和 .NET Micro Framework 等,这些表现有稍微的不同。至少有趣的 .NET Framework 有超级多个不同的版本,本考古学家也不能确定这些版本之间是否存在差异,只是听小伙伴吹过。而 .NET Micro Framework (NETMF) 本身就是设计在极度小的内存下运行,里面对引用类型做了很多有趣的优化,而我仅仅知道有优化,具体做了什么就不知道了,也不想知道

而 .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)也就是用或不用都需要这么大的空间。而栈空间会随方法的执行结束自动清空方法占用的栈空间,这部分就包含了局部变量占用的栈空间。因此使用栈空间不存在内存回收压力,也不存在内存分配的性能问题。但栈空间是很小的一段空间,一旦用完将会抛出堆栈溢出

因此本文所说的空对象占用的内存空间仅说占用的堆空间的内存空间,这不意味着本文说的对象仅仅是引用类型对象,此时值类型对象也是能包含的。但可惜的是我不准备直接讨论值类型对象在堆空间的情况

在开始之前,请让咱忽略吃鸡蛋应该从大的一头开始吃还是从小的一头开始吃的问题,从 x86 和 x64 开始比较好,这是从鸡蛋小的一头开始吃的故事。等等,怎么到了吃鸡蛋的时候了?其实我说的是大端和小端的问题哈。在 .NET Core 下,在 x86 与 x86-64 平台储存整数使用的是 Little Endian 小端法,而在 ARM 与 ARM64 平台储存整数使用的是 Big Endian 大端法。具体这两个存储方法有啥不同,请自行搜寻或看农夫的《.NET Core底层入门》 的第7章第二节

试试在 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 ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请与我联系。


http://www.taodudu.cc/news/show-5310881.html

相关文章:

  • AcWing 102. 最佳牛围栏(二分、前缀和)
  • 【杂题】超级公牛冠军赛(最大生成树)
  • 数值处理题库--Sprime
  • 超级牛人在华为的十年
  • 广元停车场系统推荐_投资12亿元!广元将在这里建造超级服务区
  • 叶国富的“超级品牌梦”,需要很多个“三丽鸥”
  • 超级食物引爆超级品类,蓝区沙棘如何引领百亿风口
  • bootstrap超级基础的用法
  • 超级计算机的英语作文80字,80字英语作文要10篇80字左右的英语作文
  • php 海关对接 进口商品_海关总署公布跨境进口统一版系统对接事项
  • 解决端口号被占用的问题
  • 中国对外贸易电子商务框架
  • Netcool为中国电子口岸构筑无忧网络管理平台
  • 【Nvidia显卡驱动安装失败】
  • 显卡驱动安装不了如何解决
  • nvidia安装程序无法如何解决
  • WIN10 NVIDIA右键无控制面板及安装驱动
  • nvidia dch 驱动 与windows 不兼容_NVIDIA不再提供DCH驱动下载 Win10装不了驱动看这里...
  • 服务器虚拟驱动安装,安装GRID驱动_Windows驱动安装_GRID驱动安装_安装NVIDIA驱动指引_快速入门_GPU云服务器-网宿科技...
  • 【转载】Win10 LTSC 2019 Nvidia DCH 驱动安装 NVIDIA Control Panel
  • 机房节能趋向系统化、智能化、精细化
  • “大服务器”拥有大智慧
  • 2018年度IT168技术卓越奖名单:服务器类
  • 大数据时代,小数据中心
  • eplan 电箱布局_基于Eplan的电柜三维布线的应用
  • 布线品牌纷纷推出机柜的背后
  • 机柜卖出“奔驰”价
  • 2016全球与中国市场服务器机柜深度研究报告
  • 团队管理经验学习总结
  • 三个小思路:让团队任务管理更高效,提升团队战斗力

C# CLR 聊聊对象的内存布局 一个空对象占用多少内存相关推荐

  1. php 返回一个空对象,PHP如何定义一个空对象

    使用新数组我这样做: $aVal = array(); $aVal[key1][var1] = "something"; $aVal[key1][var2] = "som ...

  2. DOM对象本身也是一个js对象,所以严格来说,并不是操作这个对象慢,而是说操作了这个对象后,会触发一些浏览器行为(转)...

    一直都听说DOM很慢,要尽量少的去操作DOM,于是就想进一步去探究下为什么大家都会这样说,在网上学习了一些资料,这边整理出来. 首先,DOM对象本身也是一个js对象,所以严格来说,并不是操作这个对象慢 ...

  3. php 怎么定义一个空对象,php定义空对象的方法

    本文主要和大家分享php定义空对象的方法,有时候我们直接对不存在的数组直接定义其下标的值,不会报错,但是我们定义不存在的对象的时候,就会报错,这个时候我们定义一个空对象即可.有以下三种方法:<? ...

  4. 获取内存_如何获取一个进程所占用的内存

    推荐观看: BATJ面试官最喜欢问的:多线程.线程并发面试题详解(volatile+ThreadLocal+Sleep)_哔哩哔哩 (゜-゜)つロ 干杯~-bilibili​www.bilibili. ...

  5. 在c++中一个空类占用的字节数

    结论:  在c++中一个空类占用:1Byte. 有的同学认为一个空类占0Byte,那么定义一个空类时,他存放到哪里呢?为此,在c++编译器中,为空类设置了占用1Byte内存. #include < ...

  6. 1 OC 对象的本质(一个NSObject 对象占用的内存大小)

    1 前言 目录 1 前言 2 一个NSObject占用多少内存 3 为什么呢 ? 4 如何在内存中看呢? OC 的面向对象都是基于C/C++ 的数据结构实现的 结构体 2 clang 命令转换成c++ ...

  7. linux程序内存布局,关于Linux中程序的内存布局

    我有一些关于Linux中的程序的内存布局的问题. 我知道从各种来源(我正在读"从头开始编程"),每个部分都加载到它自己的内存区域. 文本部分首先在虚拟地址0x8048000处加载, ...

  8. 共享内存 传一个类指针_共享内存介绍:Shared Memory

    In computer hardware, shared memory refers to a (typically large) block of random access memory (RAM ...

  9. Android哪个系统占用内存小,哪个安卓模拟器占用的内存小,使用起来又不卡

    电脑上使用安卓模拟器想要不卡的话需要兼容的模拟器,那么哪个安卓模拟器占用的内存小,使用起来又不卡?本文给各位介绍几款以供参考. 1.叶子猪手游模拟器 首先我们将来说说叶子猪手游模拟器,相信喜欢玩游戏的 ...

最新文章

  1. 打印循环换行_科学向日葵在线课堂 ——张老师讲Python 第八课 周而复始为循环2...
  2. (已解决)RuntimeError: CUDA error: device-side assert triggered CUDA kernel errors
  3. 程序员面试题精选100题(62)-C/C++/C#面试题(5)
  4. 深入理解javascript原型和闭包(9)——简述【执行上下文】下
  5. centos Error: Cannot find a valid baseurl for repo: base 解决方法
  6. 程序员求助:被领导强行要求写Bug该怎么办?网友的回答让我笑翻
  7. python备份虚拟环境
  8. Day 20:网络编程(1)
  9. (58)Verilog HDL事件或:or、,
  10. 【BZOJ-4524】伪光滑数 堆 + 贪心 (暴力) [可持久化可并堆 + DP]
  11. 如何自制一款智能AI离线语音小夜灯
  12. html遮罩层样式,遮罩层样式
  13. 一周新书榜:程序员最受关注的书有这样6本
  14. android 第三方视频库,android视频播放库
  15. AdSense 使用教程: 通过让 Google 在您的网站上投放广告,帮助您从中获利
  16. JS随手记——跨页面传值
  17. mysql删除某张表三个月前的数据
  18. 羊皮卷的故事-第九章-羊皮卷之二
  19. autojs-获取api接口JSON值
  20. SpringBoot集成TkMapper

热门文章

  1. [转]一步步使用3Dmax+Photoshop为游戏关卡设计瓦片化3D模型和无缝贴图(视频教程)...
  2. “微商公社”怎样使企业获得裂变式增长?
  3. stm32f103基本定时器的使用
  4. skb结构和相关操作函数
  5. 计算机设计色彩ppt课件,《多媒体技术》PPT课件.ppt
  6. 别用眼泪感动爱情-转
  7. 电厂数字化仪控系统功能测试工具ETest设计与实现
  8. python抽样不同花色纸牌_想要随机出5个不同花色和数字的扑克牌该怎么做?
  9. 2023年关于身份安全的4 个预测
  10. 【疯了Labview】(一)仿JKI的RCF 挂件