环境变量

.Net Core 8.0

Windows 11 64位

内存布局

引用类型

在.NET中,数据会按照类型分为不同的对象,对于引用类型的实例,由一个对象标头(Object Header)和方法表(MethodTable)以及字段值组成

  1. 对象标头(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时,什么都不包含,大部分对象都属于这种状态
  1. 方法表(Method Table)/类型数据

    这里是对象之间互相引用时的引用点,换句话说,如果有一个引用指向某个对象,引用者将指向该对象method table reference的地址。

    类型数据所属模块,名称,字段列表,属性列表,方法列表,各个方法的入口点地址等信息

    .NET中的反射,接口,虚方法都需要类型数据作为支撑

反射会把类型数据中的内容包装为托管对象供托管代码访问

接口与虚方法需要访问类型数据中的函数表,在执行时定位实际调用的函数地址

  1. 字段

    如果类型没有字段,当前垃圾回收器要求每个对象至少有一个指针大小的字段,作为数据占位符。但是这个字段不必专用于垃圾回收器,可以被其他用途重用。比如存储数组对象的长度,但最重要的用途还是用作于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. 类型的对齐方式是其最大元素的大小(1,2,4,8等字节)或指定的打包大小(以较小者为准)
  2. 每个字段必须与其自身大小(1,2,4,8等字节)或类型的对齐方式 对齐
  3. 在字段之间添加填充以满足对齐需求

如上所述,在MyStruct/MyClass中,最大的字段是8B的Int64,所以内存开始的地址必须可以被8整除

字段布局的设计决策

  1. 值类型:默认情况下具有顺序布局,因此字段是按照定义的顺序存储在内存中。这主要在设计之初,通常认为结构用与interop(互操作)场景。会被传递给非托管代码。其布局仍会考虑对齐要求,引入填充并增加生成的结构的大小(作为高效访问对齐字段的成本)
  2. 引用类型:默认情况下自动布局,如何布局字段又CLR管理,会以最高效的方式重新排序

自定义内存布局(StructLayout)

从上文可知,结构的内存布局与字段的顺序息息相关。所以也解释了。同样的字段,Paddings差异如此之大的原因。MyStruct整整11个字节被浪费,而MyClass仅有3个字节。如果只是偶尔使用这种结构,那问题不大,如果你的代码严重依赖于值类型,并且应该要以高性能执行的方式来处理数百万个值类型,那么此类浪费可能会带来影响。

.NET提供了一种控制字段布局的方法。这同样是面向interop(互操作)场景所设计的。

  1. LayoutKind.Sequential:顺序布局,按照字段定义的顺序存储并且保证正确的字段对齐。这是非托管结构的默认值
  2. LayoutKind.Auto:保证字段的对齐,但可以对字段进行重新排序(以高效利用内存)。这是托管类型的默认值
  3. 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#查漏补缺----对象内存结构与布局的更多相关文章

  1. linux查漏补缺-Linux文件目录结构一览表

    FHS 标准 FHS(Filesystem Hierarchy Standard),文件系统层次化标准,该标准规定了 Linux 系统中所有一级目录以及部分二级目录(/usr 和 /var)的用途. ...

  2. js基础查漏补缺(更新)

    js基础查漏补缺: 1. NaN != NaN: 复制数组可以用slice: 数组的sort.reverse等方法都会改变自身: Map是一组键值对的结构,Set是key的集合: Array.Map. ...

  3. Entity Framework 查漏补缺 (一)

    明确EF建立的数据库和对象之间的关系 EF也是一种ORM技术框架, 将对象模型和关系型数据库的数据结构对应起来,开发人员不在利用sql去操作数据相关结构和数据.以下是EF建立的数据库和对象之间关系 关 ...

  4. Java查漏补缺(3)(面向对象相关)

    Java查漏补缺(3) 继承·抽象类·接口·静态·权限 相关 this与super关键字 this的作用: 调用成员变量(可以用来区分局部变量和成员变量) 调用本类其他成员方法 调用构造方法(需要在方 ...

  5. Java基础查漏补缺(2)

    Java基础查漏补缺(2) apache和spring都提供了BeanUtils的深度拷贝工具包 +=具有隐形的强制转换 object类的equals()方法容易抛出空指针异常 String a=nu ...

  6. CSS基础面试题,快来查漏补缺

    本文大部分问题来源:50道CSS基础面试题(附答案),外加一些面经. 我对问题进行了分类整理,并给了自己的回答.大部分知识点都有专题链接(来源于本博客相关文章),用于自己前端CSS部分的查漏补缺.虽作 ...

  7. JAVA查漏补缺 2

    JAVA查漏补缺 2 目录 JAVA查漏补缺 2 面向对象编程 定义类的注意事项 两个变量指向同一个对象内存图 垃圾回收机制 面向对象编程 面向:找.拿 对象:东西 面向对象编程:找或拿东西过来编程 ...

  8. JAVA查漏补缺 1

    JAVA查漏补缺 1 目录 JAVA查漏补缺 1 基本数据类型 数组 方法参数传递机制 基本数据类型 数据类型 关键字 取值范围 内存占用(字节数) 整型 byte -128~127 1 整型 sho ...

  9. 2019Java查漏补缺(一)

    看到一个总结的知识: 感觉很全面的知识梳理,自己在github上总结了计算机网络笔记就很累了,猜想思维导图的方式一定花费了作者很大的精力,特共享出来.原文:java基础思维导图 自己学习的查漏补缺如下 ...

  10. Mysql查漏补缺笔记

    目录 查漏补缺笔记2019/05/19 文件格式后缀 丢失修改,脏读,不可重复读 超键,候选键,主键 构S(Stmcture)/完整性I(Integrity)/数据操纵M(Malippulation) ...

随机推荐

  1. 人形机器人(humanoid)(双足机器人、四足机器人)—— 操控员 —— 机器人数据收集操作员

    参考: https://www.youtube.com/watch?v=jbQ4M4SNb2M 机器人数据收集操控员,就和大模型训练数据收集员.数据类型标识员(打标签人员)一样,都是为了人工生成AI训 ...

  2. 下一代浏览器和移动自动化测试框架:WebdriverIO

    1.介绍 今天给大家推荐一款基于Node.js编写且号称下一代浏览器和移动自动化测试框架:WebdriverIO 简单来讲:WebdriverIO 是一个开源的自动化测试框架,它允许测试人员使用 No ...

  3. 【Azure Developer】使用Python SDK去Azure Container Instance服务的Execute命令的疑问解释

    Azure Container Instance服务介绍 Azure 容器实例(Azure Container Instances,简称 ACI)是一个无服务器容器解决方案,允许用户在 Azure 云 ...

  4. 执行maven时报内存溢出OutOfMemory

    解决的方法是调整java的堆大小的值. Windows环境中 找到文件%M2_HOME%\bin\mvn.bat ,这就是启动Maven的脚本文件,在该文件中你能看到有一行注释为: @REM set ...

  5. 【A GUIDE TO CRC ERROR DETECTION ALGORITHM】 (译文1)

    A GUIDE TO CRC ERROR DETECTION ALGORITHM (译文) <A PAINLESS GUIDE TO CRC ERROR DETECTION ALGORITHM& ...

  6. vs code 快速配置

    1. 基本操作 打开工程文件: ctrl + p 在文件搜索内容: ctrl + shift + f 进入设置: ctrl + shift + p, 然后输入 user setting 添加插件: c ...

  7. Redis分布式锁防止缓存击穿

    一.Nuget引入 StackExchange.Redis.DistributedLock.Redis依赖 二.使用 StackExchange.Redis 对redis操作做简单封装 public ...

  8. k8s Deployment与Service配置样例

    一.Deployment apiVersion: apps/v1 kind: Deployment metadata: name: pie-algorithm-farmland-detection s ...

  9. 我恨 gevent

    报错了一晚上,最后发现是 python 版本不对.3.11,3.12,3.8,3.10 试了个遍,最后 3.10 终于编译通过了‍ 还有这个 greenlet,每次都是它和 gevent 合着来恶心我 ...

  10. Kubernetes-1:初识k8s 什么是kubernetes

    Kubernetes简介 为什么要用k8s? 容器间(Docker)在夸主机通信时,只能通过在主机做端口映射(DNAT)来实现,这种方式对于很多集群应用来说及其不方便.会影响整体处理速度,所以引入k8 ...