《对象和人,俩个世界,一样情怀》--对象之生:
---托管堆内存分配机制
大家好~~!在上篇我们讨论了堆栈的内存分配机制,接下来呢?//我们继续讨论......。
引用类型的实例分配于托管堆上,而线程栈确是生命开始的地方。
首先:我们先来理解引用类型///引用类型(Reference Type),引用类型实例分配在托管堆(Managed heap)上,变量保存了实例数据的内
存引用。MSDN中定义为:引用类型存储对值的内存地址的引用,位于堆上。引用类型细分如图:
引用类型的实例分配于托管堆上,而线程栈确是生命周期开始的地方。对于32位的处理器来说,应用程序完成进程初始化后,CLR将在
进程的可用地址空间上分配一块保留的地址空间,它是进程(每个进程可使用4GB)中可用地址空间上的一块内存区域,但并不对应于任何物理内
存,这块地址空间即是托管堆。
托管堆又根据存储信息的不同划分为多个区域,其中最重要的是垃圾回收堆(GC heap) 和 加载堆 (Loader heap);GC heap用于存储
对象实例,受GC管理;Loader Heap用于存储类型系统,它最重要的信息就是元数据相关的信息,也就是Type对象,每个Type在Loader Heap上
体现为一个Method Table(方法表),而Method Table中则记录了存储的元数据信息,例如:基类类型、静态字段、实现的接口、、、等。它不
受GC控制,其生命周期为从创建到AppDomain(应用程序)卸载。
在进入实际内存分配分析之前,有必要对几个基本概念做个解释,以便更好的爱接下来的分析中展开讨论。(说实在的在没看书之前就
没听说过) ~.~!
TypeHandle,(类型句柄),指向对应实例的方法表,每个对象创建时都包含该附加成员,并且占用4个字节的内存空间。我们知道,每个
类型都对应于一个方法表,方法表创建于编译时,主要包含了类型的特征信息、实现的接口数目、等。
SyncBlockIndex,用于线程同步,每个对象创建时都包含该附加成员,它指向一块被称为Synchronization Block的内存块,同样占用4
个字节的内存空间。
NextObjPtr,由托管堆维护的一个指针,用于标识下一个新建对象分配时在托管堆中的所处位置。CLR初始化时,NextObjPtr位于托管堆
的基地址。
现在我们以一个相对简单的类型来说明:引用类型的内存分配情况///
#region
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;
}
//Main函数、程序入口点
public static void Main()
{
VIPUser aUser;
aUser = new VIPUser();
aUser.isVIP = true;
Console.WriteLine(aUser.IsVipUser());
Console.ReadLine();
}
}
#endregion
首先,将声明一个引用类型变量aUser:
VIPUser aUser;
它仅是一个引用(指针),保存在线程堆栈上,占用4Byte空间,将用于保存VIPUser对象的有效地址,其执行过程正是上篇描述的在线程栈上
的分配过程。此时 aUser 未指向任何有效的实例,因此被自行初始化为null,试图对aUser的任何操作将抛出 NullReferenceException 异常。
接着,通过new操作执行对象创建:
aUser = new VIPUser();
该操作对应于执行newobj指令,细分为:
(a) CLR按照其继承层次进行搜索,计算类型及所有父类字段,该搜索将一直递归到System。Object类型,并返回字节总数,以本例而言需
要的字节总数为15Byte具体计算为:VIPUser类型本身字段isVip(Bool型)为1Byte;父类User类型字段id---4Byte 字段user保存了指向UserInfo
型的引用,因此占4Byte,而同时还要为UserInfo分配6Byte字节的内存。
(b) 实例对象所占的字节总数还要加上对象附加成员所需的字节总数,其中附加成员包括:TypeHandle,SyncBlockIndex,共计8字节(z在
32位的平台下)。因此,需要在托管堆上分配的字节总数为23字节,而堆上的内存块总是按照4Byte的倍数进行分配,因此本例中将分配24字节的
地址空间。
(c)最后就是内存分配了:Gc中---NextObjPtr指针向前推进24个字节,并清零原NextObjPtr和当前NextObjPtr指针地址即可,该地址正是新
创建对象的托管地址,也就是aUser引用指向的实例地址。而此时的NextObjPtr仍指向下一个新建对象的位置。
在上述操作中如果试图分配所需空间而发现内存不足时,GC将启动垃圾收集操作来回收垃圾对象所占的内存,我们将在垃圾回收做详细的
分析。
最后调用对象构造器,进行对象初始化操作,完成创建过程。该构造过程可细分为:
构造VIPUser类型的Type对象,主要包括静态字段、方法描述、实现的接口等,并将其分配在上文提到的托管堆的loader Heap上。
初始化aUser的俩附加成员:TypeHandle和SyncBlockIndex。将TypeHandle指针指向Loader Heap上的MethodTable,CLR将根据TypeHandle定位
具体的Type; 将SyncBlockIndex指针指向SyncHronIzationBlock 的内存块,用于在多线程环境下对实例对象的同步操作。
调用VipUser的构造器,进行实例字段的初始化。实例初始化时会首先向上递归执行VipUser类为止。以本例而言,初始化过程首先执行System
。Object类,在执行User类,最后才是VipUser类。最终,newObj 分配的托管堆的内存地址,被传递个VipUser的this参数,并将其引用传给栈
上声明的aUser.请看如图:
仔细看图不难发现:栈的分配是向低地址扩展,而堆的分配则是向高地址扩展。
~.~呵呵休息下、、、在.net世界中区分编译时和运行时在接下来的文章中我会尽量把上述写到的都区分开来//谢谢大家来看我的读后感--