总的来说,.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. Hexo博客Next6.0版本主题配置(站内搜索、新建404界面、静态资源压缩、底部信息隐藏、各版块透明度修改、字数统计、推荐阅读、博文置顶、阅读进度、在线评论、运行时间)

    新建404界面 在站点根目录下,输入hexo new page 404,在默认Hexo站点下/source/404/index.md 打开新建的404界面,编辑属于自己的404界面,可以显示腾讯公益4 ...

  2. Kerberos、黄金票据与白银票据

    kerberos Kerberos是一个网络认证协议,用于验证用户和服务之间的身份,解决分布式计算环境中的身份验证问题.它使用加密技术来提供安全的身份验证,并防止网络中的身份欺骗攻击.Kerberos ...

  3. PowerShell:因为在此系统上禁止运行脚本,解决方法

    解决方案 get-executionpolicy set-executionpolicy remotesigned 输入Y 至此问题解决

  4. Django日志输出

    # 自定义日志输出信息 LOGGING = { 'version': 1, 'disable_existing_loggers': True, 'formatters': { 'standard': ...

  5. sqlmap指定参数注入

    在参数前面加星号

  6. 使用C#的窗体显示与隐藏动画效果方案 - 开源研究系列文章

    今天继续研究C#的WinForm的显示动画效果. 上次我们实现了无边框窗体的显示动画效果(见博文:基于C#的无边框窗体动画效果的完美解决方案 - 开源研究系列文章 ),这次介绍的是未在任务栏托盘中窗体 ...

  7. [go]封装go的docker镜像

    前言 多阶段封装docker镜像,使用scratch镜像,尽量减小镜像包的体积. 封装用于编译的go镜像 Dockerfile FROM golang:1.20.1 AS builder WORKDI ...

  8. 解密Prompt系列13. LLM Agent-指令微调方案: Toolformer & Gorilla

    上一章我们介绍了基于Prompt范式的工具调用方案,这一章介绍基于模型微调,支持任意多工具组合调用,复杂调用的方案.多工具调用核心需要解决3个问题,在哪个位置进行工具调用(where), 从众多工具中 ...

  9. U268603 I Hate This Tree 题解

    传送门 一道纯粹的码力 + 卡常题. 前置 矩阵乘法,线段树. 分析 线段树存矩阵. 构造迭代矩阵: \[\begin{pmatrix}f_i&f_{i-1}\end{pmatrix}\tim ...

  10. [ABC132D] Blue and Red Balls

    2023-01-16 题目 题目传送门 翻译 翻译 难度&重要性(1~10):3 题目来源 AtCoder 题目算法 dp 解题思路 因为蓝球的数量是固定的,题目让我们求,在取 \(i\) 次 ...