代码案例
public class UserInfo
{
private Int32 age = -1;
private char level = 'A';
}
public class User
{
private Int32 id;
private UserInfo user;
}
public class VIPUser : User
{
public bool isVip;
public bool IsVipUser()
{
return isVip;
}
public static void Main()
{
VIPUser aUser;
aUser = new VIPUser();
aUser.isVip = true;
Console.WriteLine(aUser.IsVipUser());
}
}
将上述代码的执行过程,反编译为 IL 语言可知:new 关键字被编译为 newobj 指令来完成对象创建工作,进而调用类型的构造器来完成其初始化操作,然后我们描述执行的具体过程:首先,将声明一个引用类型变量 aUser:
VIPUser aUser;
它仅是一个引用(指针),保存在线程的堆栈上,占用 4Byte 的内存空间,将用于保存 VIPUser 对象的有效地址,其执行过程正是上文描述的在线程栈上的分配过程。此时 aUser 未指向任何有效的实例,因此被自行初始化为 null,试图对 aUser 的任何操作将抛出 NullReferenceException 异常。
接着,通过 new 操作执行对象创建:
aUser = new VIPUser();
如上文所言,该操作对应于执行 newobj 指令,其执行过程又可细分为以下几步:
1、CLR 按照其继承层次进行搜索,计算类型及其所有父类的字段,该搜索将一直递归到System.Object 类型,并返回字节总数,以本例而言类型 VIPUser 需要的字节总数为 15Byte,具体计算为:VIPUser 类型本身字段 isVip(bool 型)为 1Byte;父类 User 类型的字段id(Int32 型)为 4Byte,字段 user 保存了指向 UserInfo 型的引用,因此占 4Byte,而同时还要为 UserInfo 分配 6Byte 字节的内存。
实例对象所占的字节总数还要加上对象附加成员所需的字节总数,其中附加成员包括 TypeHandle 和 SyncBlockIndex,共计 8 字节(在 32 位 CPU 平台下)。因此,需要在托管堆上分配的字节总数为 23 字节,而堆上的内存块总是按照 4Byte 的倍数进行分配,因此本例中将分配 24字节的地址空间。
2、CLR 在当前 AppDomain 对应的托管堆上搜索,找到一个未使用的 20 字节的连续空间,并为其分配该内存地址。事实上,GC 使用了非常高效的算法来满足该请求,NextObjPtr 指针只需要向前推进 20 个字节,并清零原 NextObjPtr 指针和当前 NextObjPtr 指针之间的字节,然后返回原 NextObjPtr 指针地址即可,该地址正是新创建对象的托管堆地址,也就是 aUser 引用指向的实例地址。而此时的 NextObjPtr 仍指向下一个新建对象的位置。注意,栈的分配是向低地址扩展,而堆的分配是向高地址扩展。
注意:另外,实例字段的存储是有顺序的,由上到下依次排列,父类在前子类在后。在上述操作时,如果试图分配所需空间而发现内存不足时,GC 将启动垃圾收集操作来回收垃圾对象所占的内存。
最后,调用对象构造器,进行对象初始化操作,完成创建过程。该构造过程,又可细分为以下几个环节:
(a)构造 VIPUser 类型的 Type 对象,主要包括静态字段、方法表、实现的接口等,并将其分配在上文提到托管堆的 Loader Heap 上。
(b)初始化 aUser 的两个附加成员:TypeHandle 和 SyncBlockIndex。将 TypeHandle 指针指向 Loader Heap 上的 MethodTable,CLR 将根据 TypeHandle 来定位具体的 Type;将SyncBlockIndex 指针指向 Synchronization Block 的内存块,用于在多线程环境下对实例对象的同步操作。
(c)调用 VIPUser 的构造器,进行实例字段的初始化。实例初始化时,会首先向上递归执行父类初始化,直到完成 System.Object 类型的初始化,然后再返回执行子类的初始化,直到执行 VIPUser 类为止。以本例而言,初始化过程为首先执行 System.Object 类,再执行 User类,最后才是 VIPUser 类。最终,newobj 分配的托管堆的内存地址,被传递给 VIPUser 的 this 参数,并将其引用传给栈上声明的 aUser。
上述过程,基本完成了一个引用类型创建、内存分配和初始化的整个流程,然而该过程只能看作是一个简化的描述,实际的执行过程更加复杂,涉及到一系列细化的过程和操作。
对象创建并初始化之后,内存的布局,可以表示为:
总结
综上分析可知,在托管堆中增加新的实例对象,只是将 NextObjPtr 指针增加一定的数值,再次新增的对象将分配在当前 NextObjPtr 指向的内存空间,因此在托管堆栈中,连续分配的对象在内存中一定是连续的,这种分配机制非常高效。