总的来说,.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中的数组在内存中如何布局?的更多相关文章

  1. Java中数组在内存中的图解

    Java中的数组在内存中的图解,其实对于数组,还是比较熟悉的,平时用的也是很多的,在看数据结构与算法的极客时间专栏,最常用的10个数据结构:数组.链表.栈.队列.散列表.二叉树.堆.跳表.图.Trie ...

  2. JVM中,对象在内存中的布局

    在hotSpot虚拟机中,对象在内存中的布局可以分成对象头.实例数据.对齐填充三部分. 对象头:主要包括: 1.对象自身的运行行元数据,比如哈希码.GC分代年龄.锁状态标志等,这部分长度在32位虚拟机 ...

  3. Sliverlight linq中的数组筛选数据库中的数据

    首先 什么是linq呢 ? LINQ即Language Integrated Query(语言集成查询),LINQ是集成到C#和Visual Basic.NET这些语言中用于提供查询数据能力的一个新特 ...

  4. C#不允许在foreach循环中改变数组或集合中元素的值(注:成员的值不受影响)

    C#不允许在foreach循环中改变数组或集合中元素的值(注:成员的值不受影响),如以下代码将无法通过编译. foreach (int x in myArray) { x++; //错误代码,因为改变 ...

  5. Java数组在内存中是如何存放的

    阅读目录 一维数组 二维数组 数组对象及其引用存放在内存中的哪里? Java中有两种类型的数组: 基本数据类型数组: 对象数组: 当一个对象使用关键字“new”创建时,会在堆上分配内存空间,然后返回对 ...

  6. Java 数组在内存中的结构

    Java中的数组存储两类事物: 原始值(int,char,...),或者引用(对象指针). 当一个对象通过 new 创建,那么将在堆内存中分配一段空间,并且返回其引用(指针). 对于数组,也是同样的方 ...

  7. C# 数组在内存中的存储

    C# 数组是引用类型,那么在内存中是如何存储的呢? 在VS中调试C#程序,如何查看内存.寄存器.反汇编 在这篇文章里看到了如何在VS 中查看内存 先断点打在数组创建后语句那里,点debug->W ...

  8. C#基础数据类型与字节数组(内存中的数据格式)相互转换(BitConverter 类)

      在某种通讯协议中(如 Modbus),可能需要把一些基本的数据类型内存中的表示形式转换成以字节数组的形式,方便传送.C/C++中可以利用指针等操作完成,但C#中没有指针,咋办呢?可以用BitCon ...

  9. js的数组在内存中是如何存储的

    前言:本来想自己总结下,但发现以下文章已经写得很好,就直接放链接了. 英文文章:http://voidcanvas.com/javascript-array-evolution-performance ...

  10. JavaScript 之 数组在内存中的存储方式(连续或不连续)

    最近在纠结一个问题,就是数组这个引用类型在JavaScript 中是不是和其他语言一样开辟了一个连续的内存来存储,但是在JS 中每个元素又可以是不同的类型,这就导致了没办法用一个相同大小的存储,所以数 ...

随机推荐

  1. Cookie相关基础

    1) 创建Cookie对象, public Cookie(java.lang.String name, java.lang.String value) 参数1:表示cookie名称 参数2:表示coo ...

  2. PB从入坑到放弃(四)常用函数

    写在前面 这一期呢,来整理下PB 常用的函数,包括系统的和一些自己封装好的函数 一.字符串相关 1.1 Len函数 获取字符串长度 ① 语法 Len(string) ②参数 string-->s ...

  3. PE结构总览

    pe文件经历了从16位系统到32位系统的过度.因此32系统下的每一个PE文件都可以在16位系统下运行. 16位系统下的PE结构 在16位系统下,PE结构可以大致分为两个部分:DOS头和一些其他数据 # ...

  4. PostgreSQL 9.6 文档: 数据类型

    章 8. 数据类型 目录 8.1. 数字类型 8.1.1. 整数类型 8.1.2. 任意精度数字 8.1.3. 浮点类型 8.1.4. 序数类型 8.2. 货币类型 8.3. 字符类型 8.4. 二进 ...

  5. mysql中使用sql语句统计日志计算每天的访问量

    日志建表语句: CREATE TABLE `syslog` ( `syslogid` int(11) NOT NULL AUTO_INCREMENT, `username` varchar(255) ...

  6. Gin中间件开发

    Gin是一个用Go语言编写的Web框架,它提供了一种简单的方式来创建HTTP路由和处理HTTP请求.中间件是Gin框架中的一个重要概念,它可以用来处理HTTP请求和响应,或者在处理请求之前和之后执行一 ...

  7. Linux 网络发包流程

    哈喽大家好,我是咸鱼 之前咸鱼在<Linux 网络收包流程>一文中介绍了 Linux 是如何实现网络接收数据包的 简单回顾一下: 数据到达网卡之后,网卡通过 DMA 将数据放到内存分配好的 ...

  8. 2、搭建MyBatis

    2.1.开发环境 IDE:idea 2019.2 构建工具:maven 3.8.4 MySQL版本:MySQL 5.7 MyBatis版本:MyBatis 3.5.7 MySQL不同版本的注意事项 ( ...

  9. 记一次weak_up函数绕过

    2023 蓝帽杯CTF LovePHP 因为比赛已经结束,所以复现环境是从本地进行复现,这次比赛本来排名挺靠前的,原本总排名是60多名,赛区排名30多名,本来是以为有希望进入半决赛的,但是没想到比赛结 ...

  10. ThreadLocal:线程中的全局变量

    最近接了一个新需求,业务场景上需要在原有基础上新增2个字段,接口新增参数意味着很多类和方法的逻辑都需要改变,需要先判断是否属于该业务场景,再做对应的逻辑.原本的打算是在入口处新增变量,在操作数据的时候 ...