在.Net中,Object是所有类型的父类。任何一个类型可以赋值给Object类型的变量。此时当将值类型的赋值给Object类型变量,实际上会进行一次隐式类型转化。将值类型转化为引用类型就会发生装箱,反之引用类型转化为值类型就会发生拆箱。程序运行期间所有的数据实际上都会存放于内存(不考虑寄存器),所以,咱们从内存的角度去看一下装箱、拆箱做了哪些操作。
示例代码如下
using System.Diagnostics;
var person = new Person()
{
Age = 10,
Name = "张三",
};
Object personObj = person;
person.Age = 11;
Person personNew=(Person)personObj;
personNew.Age = 12;
Console.WriteLine($"personObj:{personObj}");
Console.WriteLine($"person:{person}");
Console.WriteLine($"personNew:{personNew}");
Debugger.Break();
public struct Person
{
public string Name { get; set; }
public int Age { get; set; }
public override string ToString()
{
return $"Name:{Name},Age:{Age}";
}
}
定义一个Person结构体,然后对其进行装箱,最后将装箱后的值拆箱,并且对各个值进行修改做。测试结果如下:
接下来,通过Windbg观察这几段代码再内存上做了什么操作。
首先通过 !dumpheap -type Person 在托管堆上找到装箱后的Obj对象实例,在观察该实例在内存上的结构,如下:
通过上述的操作,我们找到Person的Obj实例在内存中的位置,最后观察其结构,发现首位置为空,第二部分为Person方法表指针,剩下的就是字符串指针以及int值10。该结构与一般的对象实例无异。
接下来观察线程栈上Person值的结构。
首先通过k找到函数调用栈
然后通过指令k观察main方法的函数栈,因为在上述代码中,所有的Person值(包括值类型、引用类型)的Name值都是张三,所以可以通过字符串张三的指针值去查找两个值类型在函数栈上的位置。如下:
可以发现栈上有2处结构体,下面对应是第一个Person,它的Age字段赋值被为11,正好对应十六进制的0b,上面的0c对应十六进制的12。
这里面有2个地方解释下:
字符串张三的指针值为000001fa801004a0,因为内存按照从低位向高的方向存储,所以查询时使用a0 04 10 80 fa 01。
函数栈实际上也是一段内存,栈底的位置大于栈顶。也就是说从高内存到低内存存放数据。所以,上述图片显示数据时第一个Person是在下面,因为代码最先执行到它,先将第一个Person值入栈。
从上述可以看出:
装箱:将结构体的字段成员的值copy到指定内存位置,且在这些字段成员前加上2个指针位,即对象头以及方法表指针。当然,这些操作都是在内存堆空间足够的情况下进行的。
拆箱:将引用类实例的字段值copy到函数调用栈。
因为装箱、拆箱会涉及到额外的内存分配以数据copy操作,所以大家对装箱、拆箱的映像总是性能比较差,因为.Net出现了泛型避免了这些额外的操作。