首页 > 系统相关 >.NET中的数组在内存中如何布局?

.NET中的数组在内存中如何布局?

时间:2023-10-30 09:02:03浏览次数:37  
标签:TypeHandle 00 字节 内存 数组 类型 NET

总的来说,.NET的值类型和引用类型都映射一段连续的内存片段。不过对于值类型对象来说,这段内存只需要存储其字段成员,而对应引用类型对象,还需要存储额外的内容。就内存布局来说,引用类型有两个独特的存在,一个是字符串,另一个就是数组。我在《你知道.NET的字符串在内存中是如何存储的吗?》一文中对其内存布局作了详细介绍,今天我们来聊聊数组类型的内存布局。

一、引用类型布局
二、数组类型布局
三、值类型数组
四、引用类型数组

一、引用类型布局

但是对于引用类型对象,除了存储其所有字段成员外,还需要存储一个Object Header和TypeHandle,前者可以用来存储Hash值,也可以用来存储同步状态;后者存储的是目标类型方法表的地址(详细介绍可以参考我的文章《如何计算一个实例占用多少内存?》、《如何将一个实例的内存二进制内容读出来?》。

如下图所示,对于32位(x86)系统,Object Header和TypeHandle各占据4个字节;但是对于64位(x64)来说,存储方法表指针的TypeHandle自然扩展到8个字节,但是Object Header依然是4个字节,为了确保TypeHandle基于8字节的内存对齐,所以会前置4个字节的“留白(Padding)”。

image_thumb2

顺便说一下,即使没有定义任何的字段成员,运行时依然会使用一个“指针宽度(IntPtr.Size)”的存储空间(上图中的Payload),所以x86/x64系统中一个引用类型对象至少占据12/24字节的内存。除此之外,所谓对象的引用并不是指向这段内存的起始位置,而是指向TypeHandle的地址。

二、数组类型布局

既然数组是引用类型,它自然按照上面的方式进行内存布局。它依然拥有4字节的Object Header,TypeHandle部分存储的是数组类型自身的方法表地址。其荷载内容(Payload)采用如下的布局:前置4个字节以UInt32的形式存储数组的长度,后面依次存储每个数组元素的内容。对于64位(x64)来说,为了确保数组元素的内存对齐,两者之间具有4个字节的Padding。

image_thumb5

三、值类型数组

对于值类型的数组,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

标签:TypeHandle,00,字节,内存,数组,类型,NET
From: https://www.cnblogs.com/artech/p/array-memory-layout.html

相关文章

  • java 内存分配
    ......
  • 使用telnet来调试游戏
    telnet是什么Telnet协议是TCP/IP协议族中的一员,是Internet远程登陆服务的标准协议和主要方式。它为用户提供了在本地计算机上完成远程主机工作的能力但是,telnet因为采用明文传送报文,安全性不好,很多Linux服务器都不开放telnet服务,而改用更安全的ssh方式了。但仍然有很多别的系统......
  • Net 高级调试之一:开始认识一些调试工具
    一、简介从今天开始一个长系列,Net高级调试的相关文章,我自从学习了之后,以前很多模糊的地方现在很清楚了,原来自己的功力还是不够,所以有很多不明白,通过学习Net高级调试,眼前豁然开朗,茅塞顿开。其实,刚开始要学习《Net高级调试》,还是很是很困难的,很多工具不会用,又不知道如......
  • BM72 连续子数组的最大和
    描述输入一个长度为n的整型数组array,数组中的一个或连续多个整数组成一个子数组,子数组最小长度为1。求所有子数组的和的最大值。数据范围:1<=n<=2\times10^51<=n<=2×105-100<=a[i]<=100−100<=a[i]<=100要求:时间复杂度为 O(n)O(n),空间复杂度为 O(n)O(n)进阶......
  • Java基础 InetAddress
    publicstaticvoidmain(String[]args)throwsException{//address是IP的对象,也是一台电脑的对象InetAddressaddress=InetAddress.getByName("172.18.153.251");System.out.println(address.getHostName());System.out.println(address.getHostAddress()......
  • Windows 10中,可以使用以下PowerShell脚本来禁用Internet Explorer的Javascript错误提
    Windows10中,可以使用以下PowerShell脚本来禁用InternetExplorer的Javascript错误提示禁用脚本调试器Set-ItemProperty-Path"HKCU:\Software\Microsoft\InternetExplorer\Main"-Name"DisableScriptDebugger"-Value"yes"禁用每个脚本错误的通知Set-ItemProperty......
  • java 数组常见问题
    当访问了数组中不存在的索引,就会引发索引越界异常。索引越界异常原因:访问了不存在的索引避免:索引的范围最小索引:0最大索引:4(数组的长度-1)......
  • java 动态数组初始化
    动态初始化:初始化时只指定数组长度,由系统为数组分配初始值。格式:数据类型[]数组名=new数据类型[数组长度];示例:int[]arr=newint[3];publicclassday8_06{publicstaticvoidmain(String[]args){/*定义一个数组,用来存班级中50个学生的姓名......
  • java 数组遍历
    数组遍历:将数组中所有的内容取出来,取出来之后可以(打印,求和,判断..)注意:遍历指的是取出数据的过程,不要局限的理解为,遍历就是打印!publicclassday8_04{publicstaticvoidmain(String[]args){//定义数组int[]arr={1,2,3,4,5,6,7,8,9,10};......
  • 将所有的零移动到数组的末尾并保持非零元素的顺序的两种思路及JAVA代码实现
    //思路2:从前向后遍历数组,将非0数字放入一个集合中publicstaticvoidmoveZeroes02(int[]nums){if(nums==null||nums.length==0){return;}if(nums.length==1){return;}//......