对于本例中的对象创建,首先会在栈中声明一个指向堆中数据的指针(引用),它占用4个字节,然后调用newobj指令,搜索该类是否含有父类,如果有,则从父类开始分配内存,对于本例中,FruitPhone对象所需要的内存为4字节的string引用两个,4字节的int*2。实例对象所占的字节总数还要加上对象附加成员所需的字节总数,其中附加成员包括 TypeHandle 和 SyncBlockIndex,共计 8 字节(在 32 位 CPU 平台下),共计24字节。
CLR 在当前 AppDomain 对应的托管堆上搜索,找到一个未使用的 20 字节的连续空间,并为其分配该内存地址。事实上,GC 使用了非常高效的算法来满足该请求,NextObjPtr 指针只需要向前推进 20 个字节,并清零原 NextObjPtr 指针和当前 NextObjPtr 指针之间的字节,
然后返回原 NextObjPtr 指针地址即可,该地址正是新创建对象的托管堆地址,也就是p引用指向的实例地址。而此时的 NextObjPtr 仍指向下一个新建对象的位置。注意,栈的分配是向
低地址扩展,而堆的分配是向高地址扩展。
最后,调用对象构造器,进行对象初始化操作,完成创建过程。该构造过程,又可细分为
以下几个环节:
构造 FruitPhone 类型的 Type 对象,主要包括静态字段、方法表、实现的接口等,并将其
分配在上文提到托管堆的 Loader Heap 上。
初始化 p 的两个附加成员:TypeHandle 和 SyncBlockIndex。将 TypeHandl
e 指针指向 Loader Heap 上的 MethodTable,CLR 将根据 TypeHandle 来定位具体的 Type;
将 SyncBlockIndex 指针指向 Synchronization Block 的内存块,用于在多线程环境下对实例
对象的同步操作。
调用 FruitPhone 的构造器,进行实例字段的初始化。实例初始化时,会首先向上递归执
行父类初始化,直到完成 System.Object 类型的初始化,然后再返回执行子类的初始化,直到
执行 FruitPhone 类为止。以本例而言,初始化过程为首先执行 System.Object 类,直接执行 FruitPhone。最终,newobj 分配的托管堆的内存地址,被传递给 FruitPhone 的 thi
s 参数,并将其引用传给栈上声明的 p。
上述过程,基本完成了一个引用类型创建、内存分配和初始化的整个流程,然而该过程只能看作是一个简化的描述,实际的执行过程更加复杂,涉及到一系列细化的过程和操作。
(插入内存图像)
补充静态字段的内存分配和释放,又有何不同?
静态字段也保存在方法表中,位于方法表的槽数组后,其生命周期为从创建到 AppDomain
卸载。因此一个类型无论创建多少个对象,其静态字段在内存中也只有一份。静态字段只能由静
态构造函数进行初始化,静态构造函数确保在类型任何对象创建前,或者在任何静态字段或方法
被引用前执行,其详细的执行顺序请参考相关讨论。
在这一部分,我们首先观察对象之死,以此反思和体味人类入世的哲学,两者相比较,也会给我们更多关于自己的启示。对象的生命周期由 GC 控制,其规则大概是这样:GC 管理所有的托管堆对象,当内存回收执行时,GC 检查托管堆中不再被使用的对象,并执行内存回收操作。不被应用程序使用的对象,指的是对象没有任何引用。关于如何回收、回收的时刻,以及遍历可回收对象的算法,是较为复杂的问题,我们将在 后续进行深度探讨。不过,这个回收的过程,同样使我们感慨。大自然就是那个看不见的 GC,造物而又终将万物回收,无法改变。我们所能做到的是,将生命的周期拓宽、延长、书写得更加精彩
Reference你必须知道的.NET