曾经被“类型对象指针”和“同步块索引”困扰,网上查了很多,也还是一头雾水的状态,花了些时间,做了比较深入的了解后,想做个总结,如能给还处在迷茫中的朋友一些帮助,那我不胜荣幸,也欢迎大家提出指正,我也将不胜感激。

废话不多说,进入正题。很多人可能问,这俩(类型对象指针,同步块索引)和标题有什么关系?不是讲值类型和引用类型吗?其实关联性很强,稍后会讲到,先从最基础的开始。

值类型 and 引用类型

所谓值类型,C#的所有值类型都被称之为结构或者枚举,而引用类型都是类(CLR Via c#中是这么描述值类型和引用类型的),我在vs里查看,发现int 其实是 struct Int32,大家可以自行验证。值类型要简单的多,不在堆上分配,不涉及垃圾回收(GC),值类型在栈上分配(引用类型内部的值类型在堆上分配),所以本篇幅的重点放在引用类型上。

装箱and拆箱

从值类型转换为引用类型的过程称之为装箱,而从引用类型转换为值类型的过程称为拆箱(在CLR Via c#中定义的拆箱只是一个获取指针的过程,但我觉得拆箱具体定义并不重要,理解这个过程即可),装箱和拆箱下文也会讨论。

对象创建过程

c#创建对象使用new关键字,对于值类型,new会调用构造函数,而对于引用类型,除了调用构造函数外,还会涉及到分配堆内存空间的过程,最终会返回引用类型在堆上的起始地址(其实并不是起始地址,稍后会讲到)。由此可见,引用类型由两部分组成,一部分是在栈上保存了指向堆内存的地址(也就是一个指针),一部分是在堆上的,真正的引用类型的具体内容。那么具体内容是什么呢?当然包括引用类型的实例字段(这个大家都知道),还包含了任何托管堆上的对象都共有的两个额外成员,也就是开头提到的类型对象指针和同步块索引。这俩成员对于引用类型至关重要,后面会详细讨论。

装箱拆箱详解

由此可见,值类型转换为引用类型(装箱)绝不是把值类型字段复制到堆上这么简单,还需要初始化类型对象指针和同步块索引,这两个字段是有额外开销的,是要消耗内存的,会引发GC,在64位机器上,这俩各占8个字节(32位机器各4个字节),所以,即使一个引用类型内部为空,没有声明定义任何东西,new之后,也至少占用了16个字节(64位机器),其实一个空引用类型实际占用了24个字节(64位机器)。值得一提的是,在64位机器上,同步块索引虽然占用8字节,但是只使用了后4字节。

类型对象(方法表)指针

类型对象指针(或者叫方法表指针)是一个地址,指向.net运行时内部保存的类型数据的地址,类型数据中保存了很多有用的信息,还和反射有关,这里不详细讨论。类型对象指针可以获取对象真正的类型,gettype方法正是返回了类型对象指针获取到的对象的真正类型。比如定义一个object类型的int数组,调用gettype方法,返回的是system.int32[],也说明了对象的类型并不是在声明时指定的,而是由类型对象指针决定的,所有同一类型的引用对象的类型对象指针都指向同一个类型对象。

拓展:值类型调用gettype

到这里,会产生一个疑问,值类型是否能调用gettype方法?返回什么?因为gettype方法和类型对象指针息息相关,而值类型在栈上,没有类型对象指针,所以值类型调用gettype会产生装箱!装箱后变为引用类型会有类型对象指针,调用gettype后会产生正确的返回。值得一提的是,前面提到了栈上保存了指向堆内存的地址,这个地址正是类型对象指针的起始地址(关于引用类型的内存布局稍后会讲到)。

对象头(同步块索引)

同步块索引,位于引用类型的头部,见名知意,是同步块的索引,同步块是CLR维护的一个数据结构,和线程同步有关,但是在深入了解后,我更愿意将之称为“对象头”而不是同步块索引,因为同步块索引只是表达了对象头部信息的一部分内容,该位置还和hashcode有关,和gc,析构函数等有关。后面我将使用对象头这个称谓替代同步块索引。

对象头的结构

微软对于对象头的设计很精妙,对象头实际内容只有四个字节,前面提到了,在64位机器上,对象头虽然占用8字节,但是只使用了后4字节。对象头可以分为两部分来看,高6位和剩余26位,高6位可称之为标志位,就像开关一样,低26位会根据高4,5,6位的不同而展示不同内容,对于高6位的标志这里只做大概简介:高1位和string类型以及gc时标记对象状态有关,高2位和string类型及析构函数相关,高3位标记对象是否是固定对象(gc相关),高4位和自旋锁,线程锁相关,高5位和同步块索引和hash值相关,高6位和hash值相关。低26位可能存储对象的hash值,或者线程相关的信息,或者同步块索引等。关于对象头的更具体的内容,后续可能新开一个篇幅详细介绍,这里就不展开了,欢迎留言或私聊。

引用类型在内存中的布局

所以,引用类型的布局是对象头,紧接着是类型对象指针,在之后是类的实例字段等,而栈上的地址指向的并不是对象头,而是有一个偏移,64位机器偏移量是8字节,也就是类型对象指针的位置。

欢迎大家留言评论及私聊,共同学习共同进步。