C#查漏补缺----对象内存结构与布局
环境变量
.Net Core 8.0
Windows 11 64位
内存布局
引用类型
在.NET中,数据会按照类型分为不同的对象,对于引用类型的实例,由一个对象标头(Object Header)和方法表(MethodTable)以及字段值组成
- 对象标头(Object Header):按照CLR的描述,标头存储了“对象上的所有附加信息”。在32位平台上占用4字节,64位平台上占用8字节(仅使用后4个字节,以0填充前4个字节仅用于对齐)。4字节即32位,其中高6位用于标记附加信息
- 高1位:如果对象是string类型,标记字符串内存是否大于或者等于0x80(是否只包含ASCII)的字符,否则用于Runtime内部检查,GC标记阶段,标记对象是否被检查
- 高2位:如果对象是string类型,标记字符串内容是否需要特殊排序方式,否则标记对象析构函数(Finalizer)是否需要运行,如果调用了GC.SuppressFinalize,会设置为1.
- 高3位:标记对象是否固定(Pinned Object) ,如果有固定类型的GC句柄,则这个位会在GC过程中暂时性标记为1
- 高4位:标记对象是否被自旋锁(lock)获取线程锁
- 高5位:标记对象是否包含同步块索引(SyncBlock Index)或者Hash值
- 高6位:标记对象是否包含Hash值
对于String类型而言,前2位用于内容是否只包含ASCII字符,可以影响字符串排序时使用的算法。如果高1位成立,则表示排序时需要把字符串转为int处理。
高4,5,6位用于标记低26位保存了什么内容。有4中状态。这些状态会根据对对象所进行的操作进行切换,例如获取锁,释放锁,计算Hash值等。
这四种状态分别是
- 三个位为1,0,0时,包含通过自旋获取线程锁的AppDomainID(16-26位),进入次数(10-15位),线程ID(0-9位)
- 三个位为0,1,1时,包含Hash的缓存,引用类型的对象会在首次获取Hash值时生成一个缓存并保存到低26位
- 三个位为0,1,0时,包含同步块索引
- 三个位为0,0,0时,什么都不包含,大部分对象都属于这种状态
- 方法表(Method Table)/类型数据
这里是对象之间互相引用时的引用点,换句话说,如果有一个引用指向某个对象,引用者将指向该对象method table reference的地址。
类型数据所属模块,名称,字段列表,属性列表,方法列表,各个方法的入口点地址等信息
.NET中的反射,接口,虚方法都需要类型数据作为支撑
反射会把类型数据中的内容包装为托管对象供托管代码访问
接口与虚方法需要访问类型数据中的函数表,在执行时定位实际调用的函数地址
- 字段
如果类型没有字段,当前垃圾回收器要求每个对象至少有一个指针大小的字段,作为数据占位符。但是这个字段不必专用于垃圾回收器,可以被其他用途重用。比如存储数组对象的长度,但最重要的用途还是用作于GC。
根据上面介绍的内存布局,可以知道堆上每个对象都至少包含者3个部分。在32位runtime上,堆上最小对象将有12字节。64位runtime上24字节。
public class MyClass
{
public byte f1;
public long f2;
public int f3;
public long f4;
}
Type layout for 'MyClass'
Size: 24 bytes. Paddings: 3 bytes (%12 of empty space)
|============================|
| Object Header (8 bytes) |
|----------------------------|
| Method Table Ptr (8 bytes) |
|============================|
| 0-7: Int64 f2 (8 bytes) |
|----------------------------|
| 8-15: Int64 f4 (8 bytes) |
|----------------------------|
| 16-19: Int32 f3 (4 bytes) |
|----------------------------|
| 20: Byte f1 (1 byte) |
|----------------------------|
| 21-23: padding (3 bytes) |
|============================|
值类型
值类型代表数据本身
public struct MyStruct
{
public byte f1;
public long f2;
public int f3;
public long f4;
}
Type layout for 'MyStruct'
Size: 32 bytes. Paddings: 11 bytes (%34 of empty space)
|===========================|
| 0: Byte f1 (1 byte) |
|---------------------------|
| 1-7: padding (7 bytes) |
|---------------------------|
| 8-15: Int64 f2 (8 bytes) |
|---------------------------|
| 16-19: Int32 f3 (4 bytes) |
|---------------------------|
| 20-23: padding (4 bytes) |
|---------------------------|
| 24-31: Int64 f4 (8 bytes) |
|===========================|
内存对齐
细心的朋友可以看到,在内存分配的过程中,出现了padding这样的字眼。且paddings值还不一样,引用类型为3,值类型为11。这就引申出一个新的概念----内存对齐。
为什么要内存对齐?
现在CPU可以使用高效的代码来访问对齐的数据,访问未对齐的数据虽然可以,但需要更多的指令,因此速度会慢一点。
每种基元数据类型(int,float等)都有其自己首选的对齐方式----应该是存储它的地址的值的倍数。因此4字节的int32的具有4字节(可被4整除)的对齐方式,8字节的double具有8字节(可被8整除)的对其方式。以此类推,最简单的是1字节的char和byte.因为是1字节,所以它们存储在任何位置都是对齐的
内存布局定义的三个规则
- 类型的对齐方式是其最大元素的大小(1,2,4,8等字节)或指定的打包大小(以较小者为准)
- 每个字段必须与其自身大小(1,2,4,8等字节)或类型的对齐方式 对齐
- 在字段之间添加填充以满足对齐需求
如上所述,在MyStruct/MyClass中,最大的字段是8B的Int64,所以内存开始的地址必须可以被8整除

字段布局的设计决策
- 值类型:默认情况下具有顺序布局,因此字段是按照定义的顺序存储在内存中。这主要在设计之初,通常认为结构用与interop(互操作)场景。会被传递给非托管代码。其布局仍会考虑对齐要求,引入填充并增加生成的结构的大小(作为高效访问对齐字段的成本)
- 引用类型:默认情况下自动布局,如何布局字段又CLR管理,会以最高效的方式重新排序
自定义内存布局(StructLayout)
从上文可知,结构的内存布局与字段的顺序息息相关。所以也解释了。同样的字段,Paddings差异如此之大的原因。MyStruct整整11个字节被浪费,而MyClass仅有3个字节。如果只是偶尔使用这种结构,那问题不大,如果你的代码严重依赖于值类型,并且应该要以高性能执行的方式来处理数百万个值类型,那么此类浪费可能会带来影响。
.NET提供了一种控制字段布局的方法。这同样是面向interop(互操作)场景所设计的。
- LayoutKind.Sequential:顺序布局,按照字段定义的顺序存储并且保证正确的字段对齐。这是非托管结构的默认值
- LayoutKind.Auto:保证字段的对齐,但可以对字段进行重新排序(以高效利用内存)。这是托管类型的默认值
- LayoutKind.Explicit:不能保证任何内容的布局,因为我们显式定义了布局
[StructLayout(LayoutKind.Auto)]
public struct MyStruct
{
public byte f1;
public long f2;
public int f3;
public long f4;
}
Type layout for 'MyStruct'
Size: 24 bytes. Paddings: 3 bytes (%12 of empty space)
|===========================|
| 0-7: Int64 f2 (8 bytes) |
|---------------------------|
| 8-15: Int64 f4 (8 bytes) |
|---------------------------|
| 16-19: Int32 f3 (4 bytes) |
|---------------------------|
| 20: Byte f1 (1 byte) |
|---------------------------|
| 21-23: padding (3 bytes) |
|===========================|
自动布局的主要缺点是我们不能在Interop中使用这种结构,所以我们在编码过程中,要审时度势。如果我们仅仅需要高性能(栈分配,数据局部性,线程安全,较少空间占用)通用代码,我们根本不关心这个限制。只有在Interop中,我们才考虑默认布局而不是自动布局。
到目前为止,我们所展示的struct都是非托管的,但是结构也可以是托管的------只要向它们添加引用对象即可。如下所述,添加了一个object。struct会将默认布局改为自动布局(因为该struct从非托管对象变成了托管对象)
public struct MyStruct
{
public byte f1;
public long f2;
public int f3;
public long f4;
public object o1;
}
Type layout for 'MyStruct'
Size: 32 bytes. Paddings: 3 bytes (%9 of empty space)
|============================|
| 0-7: Object o1 (8 bytes) |
|----------------------------|
| 8-15: Int64 f2 (8 bytes) |
|----------------------------|
| 16-23: Int64 f4 (8 bytes) |
|----------------------------|
| 24-27: Int32 f3 (4 bytes) |
|----------------------------|
| 28: Byte f1 (1 byte) |
|----------------------------|
| 29-31: padding (3 bytes) |
|============================|
那么,引用类型能否也使用[StructLayout(LayoutKind.Squential)]改变自己的内存布局呢?答案也是可以的。(.NET内存管理宝典一书,说类和非托管结构的自动布局不能更改。我在.NET Core环境中是可以的,作者应该是.NET Framework环境)
[StructLayout(LayoutKind.Sequential)]
public class MyClass
{
public byte f1;
public long f2;
public int f3;
public long f4;
}
Type layout for 'MyClass'
Size: 32 bytes. Paddings: 11 bytes (%34 of empty space)
|============================|
| Object Header (8 bytes) |
|----------------------------|
| Method Table Ptr (8 bytes) |
|============================|
| 0: Byte f1 (1 byte) |
|----------------------------|
| 1-7: padding (7 bytes) |
|----------------------------|
| 8-15: Int64 f2 (8 bytes) |
|----------------------------|
| 16-19: Int32 f3 (4 bytes) |
|----------------------------|
| 20-23: padding (4 bytes) |
|----------------------------|
| 24-31: Int64 f4 (8 bytes) |
|============================|
当struct包含了具有[StructLayout(LayoutKind.Auto)]布局的其他struct时,默认布局行为也将会变更为Auto。大多数常用的内置struct(Decimal,Char,Boolean)是Sequential布局。
public struct MyStruct
{
public byte f1;
public long f2;
public int f3;
public long f4;
public MyStructSub sub;
}
[StructLayout(LayoutKind.Auto)]
public struct MyStructSub
{
public int? i;
}
Type layout for 'MyStruct'
Size: 32 bytes. Paddings: 3 bytes (%9 of empty space)
|===================================|
| 0-7: Int64 f2 (8 bytes) |
|-----------------------------------|
| 8-15: Int64 f4 (8 bytes) |
|-----------------------------------|
| 16-19: Int32 f3 (4 bytes) |
|-----------------------------------|
| 20: Byte f1 (1 byte) |
|-----------------------------------|
| 21-23: padding (3 bytes) |
|-----------------------------------|
| 24-31: MyStructSub sub (8 bytes) |
| |===============================| |
| | 0-7: Nullable`1 i (8 bytes) | |
| |===============================| |
|===================================|
但是Datetime/DateTimeOffset很神奇,它是auto布局。因此当Datetime用作用一个struct的字段时,它的布局也将更改为自动。且强制设置为Sequential也失效,并不理解为什么会这样?
更新: 这应该是.net core的一个bug.在.net core 2.X环境中,上文结论为true.在.net core 8.x中。Datetime与其他内置struct行为一致
也完全自定义内存布局,可以设置[StructLayout(LayoutKind.Explicit)]来强制规定内存布局。
[StructLayout(LayoutKind.Explicit)]
public struct MyStruct
{
[FieldOffset(0)]
public byte f1;
[FieldOffset(8)]
public long f2;
[FieldOffset(16)]
public int f3;
[FieldOffset(24)]
public long f4;
[FieldOffset(32)]
public DateTime o1;
}
Type layout for 'MyStruct'
Size: 40 bytes. Paddings: 11 bytes (%27 of empty space)
|=======================================|
| 0: Byte f1 (1 byte) |
|---------------------------------------|
| 1-7: padding (7 bytes) |
|---------------------------------------|
| 8-15: Int64 f2 (8 bytes) |
|---------------------------------------|
| 16-19: Int32 f3 (4 bytes) |
|---------------------------------------|
| 20-23: padding (4 bytes) |
|---------------------------------------|
| 24-31: Int64 f4 (8 bytes) |
|---------------------------------------|
| 32-39: DateTime o1 (8 bytes) |
| |===================================| |
| | 0-7: UInt64 _dateData (8 bytes) | |
| |===================================| |
|=======================================|
C#查漏补缺----对象内存结构与布局的更多相关文章
- linux查漏补缺-Linux文件目录结构一览表
FHS 标准 FHS(Filesystem Hierarchy Standard),文件系统层次化标准,该标准规定了 Linux 系统中所有一级目录以及部分二级目录(/usr 和 /var)的用途. ...
- js基础查漏补缺(更新)
js基础查漏补缺: 1. NaN != NaN: 复制数组可以用slice: 数组的sort.reverse等方法都会改变自身: Map是一组键值对的结构,Set是key的集合: Array.Map. ...
- Entity Framework 查漏补缺 (一)
明确EF建立的数据库和对象之间的关系 EF也是一种ORM技术框架, 将对象模型和关系型数据库的数据结构对应起来,开发人员不在利用sql去操作数据相关结构和数据.以下是EF建立的数据库和对象之间关系 关 ...
- Java查漏补缺(3)(面向对象相关)
Java查漏补缺(3) 继承·抽象类·接口·静态·权限 相关 this与super关键字 this的作用: 调用成员变量(可以用来区分局部变量和成员变量) 调用本类其他成员方法 调用构造方法(需要在方 ...
- Java基础查漏补缺(2)
Java基础查漏补缺(2) apache和spring都提供了BeanUtils的深度拷贝工具包 +=具有隐形的强制转换 object类的equals()方法容易抛出空指针异常 String a=nu ...
- CSS基础面试题,快来查漏补缺
本文大部分问题来源:50道CSS基础面试题(附答案),外加一些面经. 我对问题进行了分类整理,并给了自己的回答.大部分知识点都有专题链接(来源于本博客相关文章),用于自己前端CSS部分的查漏补缺.虽作 ...
- JAVA查漏补缺 2
JAVA查漏补缺 2 目录 JAVA查漏补缺 2 面向对象编程 定义类的注意事项 两个变量指向同一个对象内存图 垃圾回收机制 面向对象编程 面向:找.拿 对象:东西 面向对象编程:找或拿东西过来编程 ...
- JAVA查漏补缺 1
JAVA查漏补缺 1 目录 JAVA查漏补缺 1 基本数据类型 数组 方法参数传递机制 基本数据类型 数据类型 关键字 取值范围 内存占用(字节数) 整型 byte -128~127 1 整型 sho ...
- 2019Java查漏补缺(一)
看到一个总结的知识: 感觉很全面的知识梳理,自己在github上总结了计算机网络笔记就很累了,猜想思维导图的方式一定花费了作者很大的精力,特共享出来.原文:java基础思维导图 自己学习的查漏补缺如下 ...
- Mysql查漏补缺笔记
目录 查漏补缺笔记2019/05/19 文件格式后缀 丢失修改,脏读,不可重复读 超键,候选键,主键 构S(Stmcture)/完整性I(Integrity)/数据操纵M(Malippulation) ...
随机推荐
- 【Redis】01 NoSQL概述 & Redis
NoSQL概述: 1.什么是NoSQL NoSQL 是 Not Only SQL 的缩写,意即"不仅仅是SQL"的意思,泛指非关系型的数据库.强调Key-Value Stores和 ...
- 【Spring Data JPA】10 对象导航查询
定义: 查询一个记录时,也就是查询这个对象,通过这个对象查询他的关联对象 [说白了不就是从我们设置好的集合中获取不就完了吗] 环境搭建: INSERT INTO `jpa`.`cst_customer ...
- 在进行神经网络训练时需要使用的显存空间大小的预估——300MB的神经网络在训练时最少需要占用多大的显存空间
以Tensorflow为例. ======================================= 神经网络(TensorFlow举例)在GPU中训练时需要占用的内存大概有下面几部分组成: ...
- HP笔记本电脑——暗夜精灵2pro继电池鼓包后出现无法充电的问题,最后电量显示:0%可用(电源已接通,未充电)
问题如题,最近使用暗夜精灵2pro笔记本(自己17年5月1节日购买)使用了四年,使用了第二年的时候出现电池鼓包问题于是自己花了不到200元在某宝上购入电池进行替换同时更新bios,正常使用到今年8月2 ...
- 如果一个windows主机上插两个蓝牙适配器会如何???——由于 Windows 无法加载这个设备所需的驱动程序,导致这个设备工作异常。 (代码 31)——windows主机蓝牙适配器驱动错误排查
事情是这样的,在某鱼上挂了一个蓝牙适配器,是自己多年前买的,给自己的老电脑用的,那一台老电脑主板上没有自带蓝牙,于是就在某东上买了一个蓝牙适配器: 但是这几年新买的电脑都自带蓝牙,于是准备把这个适配器 ...
- 强化学习中Q-learning,DQN等off-policy算法不需要重要性采样的原因
在整理自己的学习笔记的时候突然看到了这个问题,这个问题是我多年前刚接触强化学习时候想到的问题,之后由于忙其他的事情就没有把这个问题终结,这里也就正好把这个问题重新的规整一下. 其实,这个DQN算法作为 ...
- 实验室服务彻底死机记录——硬件故障——主板pcie槽坏掉或显卡坏掉
2022年11月8日 后记(最新更新) 服务器送售后,售后给厂家技术打电话,厂家技术说可能是显卡的电源线松了,于是我们打开机箱把显卡的电源线紧了紧,然后神奇的事情发生了,故障解除了...... 一 ...
- Ubuntu系统anaconda报错version `GLIBCXX_3.4.30' not found
参考文章: https://blog.csdn.net/zhu_charles/article/details/75914060 =================================== ...
- MFC实现屏幕截屏
屏幕截屏 void CMainFormDlg::GetScreenPic(Rect area, OUT Mat &img, float rate, bool gray) { CDC *pDC ...
- grpc断路器之sentinel
荐
背景 为了防止下游服务雪崩,这里考虑使用断路器 技术选型 由于是springboot服务且集成了istio,这里考虑三种方案 istio hystrix sentinel 这里分别有这几种方案的对比 ...