.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部分直接存储元素自身的值。如下程序演示了如何将一个数组(Int32)对象在内存中的字节序列读出来。如代码片段所示,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(字节)。中间高亮的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
.NET中的数组在内存中如何布局?的更多相关文章
- Java中数组在内存中的图解
Java中的数组在内存中的图解,其实对于数组,还是比较熟悉的,平时用的也是很多的,在看数据结构与算法的极客时间专栏,最常用的10个数据结构:数组.链表.栈.队列.散列表.二叉树.堆.跳表.图.Trie ...
- JVM中,对象在内存中的布局
在hotSpot虚拟机中,对象在内存中的布局可以分成对象头.实例数据.对齐填充三部分. 对象头:主要包括: 1.对象自身的运行行元数据,比如哈希码.GC分代年龄.锁状态标志等,这部分长度在32位虚拟机 ...
- Sliverlight linq中的数组筛选数据库中的数据
首先 什么是linq呢 ? LINQ即Language Integrated Query(语言集成查询),LINQ是集成到C#和Visual Basic.NET这些语言中用于提供查询数据能力的一个新特 ...
- C#不允许在foreach循环中改变数组或集合中元素的值(注:成员的值不受影响)
C#不允许在foreach循环中改变数组或集合中元素的值(注:成员的值不受影响),如以下代码将无法通过编译. foreach (int x in myArray) { x++; //错误代码,因为改变 ...
- Java数组在内存中是如何存放的
阅读目录 一维数组 二维数组 数组对象及其引用存放在内存中的哪里? Java中有两种类型的数组: 基本数据类型数组: 对象数组: 当一个对象使用关键字“new”创建时,会在堆上分配内存空间,然后返回对 ...
- Java 数组在内存中的结构
Java中的数组存储两类事物: 原始值(int,char,...),或者引用(对象指针). 当一个对象通过 new 创建,那么将在堆内存中分配一段空间,并且返回其引用(指针). 对于数组,也是同样的方 ...
- C# 数组在内存中的存储
C# 数组是引用类型,那么在内存中是如何存储的呢? 在VS中调试C#程序,如何查看内存.寄存器.反汇编 在这篇文章里看到了如何在VS 中查看内存 先断点打在数组创建后语句那里,点debug->W ...
- C#基础数据类型与字节数组(内存中的数据格式)相互转换(BitConverter 类)
在某种通讯协议中(如 Modbus),可能需要把一些基本的数据类型内存中的表示形式转换成以字节数组的形式,方便传送.C/C++中可以利用指针等操作完成,但C#中没有指针,咋办呢?可以用BitCon ...
- js的数组在内存中是如何存储的
前言:本来想自己总结下,但发现以下文章已经写得很好,就直接放链接了. 英文文章:http://voidcanvas.com/javascript-array-evolution-performance ...
- JavaScript 之 数组在内存中的存储方式(连续或不连续)
最近在纠结一个问题,就是数组这个引用类型在JavaScript 中是不是和其他语言一样开辟了一个连续的内存来存储,但是在JS 中每个元素又可以是不同的类型,这就导致了没办法用一个相同大小的存储,所以数 ...
随机推荐
- LAL v0.36.7发布,Customize Sub,我有的都给你
Go语言流媒体开源项目 LAL 今天发布了v0.36.7版本. LAL 项目地址:https://github.com/q191201771/lal 老规矩,简单介绍一下: ▦ Customize S ...
- 微服务探索之路06篇k8s配置文件Yaml部署Redis使用Helm部署MongoDB和kafka
1 安装Redis 1.1创建配置文件redis.conf 切换到自己的目录下如本文是放在/home/ubuntu下 cd /home/ubuntu vim redis.conf bind 0.0.0 ...
- Linux 修改文本dos/unix 格式
使用 vi 命令打开文件,使用 :set ff 查看格式 如果显示dos,可使用 :set ff=unix 修改 pem 证书,使用 cat xx.key xx.crt DigiCertCA.crt ...
- 磁盘 io 测试
磁盘 io 测试 参考链接 主机的磁盘io测试
- 2021-7-29 MySql多表查询详解
多表连接 左连接:返回第一张表的所有数据项然后拼接第二张表(左表全有,右表对应左表才有) 右连接:返回第二张表的所有数据项然后拼接第一张表(右表全有,左表对应右表才有) 内连接:返回两张表数据相等的数 ...
- 基于C#的窗体阴影效果方案 - 开源研究系列文章
最近在研究C#的Winform窗体的效果,上次介绍了窗体动画效果的博文( 基于C#的无边框窗体动画效果的完美解决方案 - 开源研究系列文章 ),这次将窗体阴影效果的方案进行一个介绍. 找了一下度娘,具 ...
- CVE-2020-0796 SMB远程代码执行漏洞复现
前言: 这个windows的永恒之黑漏洞,不得不复现一下啦! 这个漏洞诸多大佬都已经复现了,现在跟随大佬的脚步,逐个复现一下: 可参考:https://www.adminxe.com/1220.htm ...
- TRL 正式推出,来训练你的首个 RLHF 模型吧!
我们正式向大家介绍 TRL--Transformer Reinforcement Learning.这是一个超全面的全栈库,包含了一整套工具用于使用强化学习 (Reinforcement Learni ...
- Typescript - 索引签名
1 索引签名概述 在 TypeScript 中,索引签名是一种定义对象类型的方式,它允许我们使用字符串或数字作为索引来访问对象的属性. 1.1 索引签名的定义和作用 索引签名通过以下语法进行定义: { ...
- Pandas 使用教程 Series、DataFrame
目录 Series (一维数据) 指定索引值 使用 key/value 对象,创建对象 设置 Series 名称参数 DataFrame(二维数据) 使用字典(key/value)创建 loc 属性返 ...