[你必须知道的.NET] 第十九回:对象创建始末(下)

系列文章导航:

[你必须知道的.NET] 开篇有益

[你必须知道的.NET] 第一回:恩怨情仇:is和as

[你必须知道的.NET] 第二回:对抽象编程:接口和抽象类

[你必须知道的.NET] 第三回:历史纠葛:特性和属性

[你必须知道的.NET] 第四回:后来居上:class和struct

[你必须知道的.NET] 第五回:深入浅出关键字---把new说透

[你必须知道的.NET] 第六回:深入浅出关键字---base和this

[你必须知道的.NET] 第七回:品味类型---从通用类型系统开始

[你必须知道的.NET] 第八回:品味类型---值类型与引用类型(上)-内存有理

[你必须知道的.NET] 第九回:品味类型---值类型与引用类型(中)-规则无边

[你必须知道的.NET] 第十回:品味类型---值类型与引用类型(下)-应用征途

[你必须知道的.NET] 第十一回:参数之惑---传递的艺术(上)

[你必须知道的.NET] 第十二回:参数之惑---传递的艺术(下)

[你必须知道的.NET] 第十三回:从Hello, world开始认识IL

[你必须知道的.NET] 第十四回:认识IL代码---从开始到现在

[你必须知道的.NET] 第十五回:继承本质论

[你必须知道的.NET] 第十六回:深入浅出关键字---using全接触

[你必须知道的.NET] 第十七回:貌合神离:覆写和重载

[你必须知道的.NET] 第十八回:对象创建始末(上)

[你必须知道的.NET] 第十九回:对象创建始末(下)

[你必须知道的.NET]第二十回:学习方法论

[你必须知道的.NET]第二十一回:认识全面的null

[你必须知道的.NET]第二十二回:字符串驻留(上)---带着问题思考

[你必须知道的.NET]第三十二回,深入.NET 4.0之,Tuple一二


  本文将介绍以下内容:

  • 对象的创建过程

  • 内存分配分析

  • 内存布局研究

  接上回[第十八回:对象创建始末(上)],继续对对象创建话题的讨论>>>

  2.2 托管堆的内存分配机制

  引用类型的实例分配于托管堆上,而线程栈却是对象生命周期开始的地方。对32位处理器来说,应用程序完成进程初始化后,CLR将在进程的可用地址空间上分配一块保留的地址空间,它是进程(每个进程可使用4GB)中可用地址空间上的一块内存区域,但并不对应于任何物理内存,这块地址空间即是托管堆。

  托管堆又根据存储信息的不同划分为多个区域,其中最重要的是垃圾回收堆(GC Heap)和加载堆(Loader Heap),GC Heap用于存储对象实例,受GC管理;Loader Heap又分为High-Frequency Heap、Low-Frequency Heap和Stub Heap,不同的堆上又存储不同的信息。Loader Heap最重要的信息就是元数据相关的信息,也就是Type对象,每个Type在Loader Heap上体现为一个Method Table(方法表),而Method Table中则记录了存储的元数据信息,例如基类型、静态字段、实现的接口、所有的方法等等。Loader Heap不受GC控制,其生命周期为从创建到AppDomain卸载。

  在进入实际的内存分配分析之前,有必要对几个基本概念做以交代,以便更好的在接下来的分析中展开讨论。

  • TypeHandle,类型句柄,指向对应实例的方法表,每个对象创建时都包含该附加成员,并且占用4个字节的内存空间。我们知道,每个类型都对应于一个方法表,方法表创建于编译时,主要包含了类型的特征信息、实现的接口数目、方法表的slot数目等。

  • SyncBlockIndex,用于线程同步,每个对象创建时也包含该附加成员,它指向一块被称为Synchronization Block的内存块,用于管理对象同步,同样占用4个字节的内存空间。

  • NextObjPtr,由托管堆维护的一个指针,用于标识下一个新建对象分配时在托管堆中所处的位置。CLR初始化时,NextObjPtr位于托管堆的基地址。

  因此,我们对引用类型分配过程应该有个基本的了解,由于本篇示例中FileStream类型的继承关系相对复杂,在此本文实现一个相对简单的类型来做说明:

Code

系列文章导航:

[你必须知道的.NET] 开篇有益

[你必须知道的.NET] 第一回:恩怨情仇:is和as

[你必须知道的.NET] 第二回:对抽象编程:接口和抽象类

[你必须知道的.NET] 第三回:历史纠葛:特性和属性

[你必须知道的.NET] 第四回:后来居上:class和struct

[你必须知道的.NET] 第五回:深入浅出关键字---把new说透

[你必须知道的.NET] 第六回:深入浅出关键字---base和this

[你必须知道的.NET] 第七回:品味类型---从通用类型系统开始

[你必须知道的.NET] 第八回:品味类型---值类型与引用类型(上)-内存有理

[你必须知道的.NET] 第九回:品味类型---值类型与引用类型(中)-规则无边

[你必须知道的.NET] 第十回:品味类型---值类型与引用类型(下)-应用征途

[你必须知道的.NET] 第十一回:参数之惑---传递的艺术(上)

[你必须知道的.NET] 第十二回:参数之惑---传递的艺术(下)

[你必须知道的.NET] 第十三回:从Hello, world开始认识IL

[你必须知道的.NET] 第十四回:认识IL代码---从开始到现在

[你必须知道的.NET] 第十五回:继承本质论

[你必须知道的.NET] 第十六回:深入浅出关键字---using全接触

[你必须知道的.NET] 第十七回:貌合神离:覆写和重载

[你必须知道的.NET] 第十八回:对象创建始末(上)

[你必须知道的.NET] 第十九回:对象创建始末(下)

[你必须知道的.NET]第二十回:学习方法论

[你必须知道的.NET]第二十一回:认识全面的null

[你必须知道的.NET]第二十二回:字符串驻留(上)---带着问题思考

[你必须知道的.NET]第三十二回,深入.NET 4.0之,Tuple一二


另外,实例字段的存储是有顺序的,由上到下依次排列,父类在前子类在后,详细的分析请参见[第十五回:继承本质论]。

 

  在上述操作时,如果试图分配所需空间而发现内存不足时,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指向的内存空间,因此在托管堆栈中,连续分配的对象在内存中一定是连续的,这种分配机制非常高效。

  2.3 必要的补充

  有了对象创建的基本流程概念,下面的几个问题时常引起大家的思考,在此本文一并做以探索:

  • 值类型中的引用类型字段和引用类型中的值类型字段,其分配情况又是如何?

  这一思考其实是一个问题的两个方面:对于值类型嵌套引用类型的情况,引用类型变量作为值类型的成员变量,在堆栈上保存该成员的引用,而实际的引用类型仍然保存在GC堆上;对于引用类型嵌套值类型的情况,则该值类型字段将作为引用类型实例的一部分保存在GC堆上。在[ 第八回:品味类型---值类型与引用类型(上)-内存有理]一文对这种嵌套结构,有较详细的分析。对于值类型,你只要记着它总是分配在声明它的地方。

  • 方法保存在Loader Heap的MethodTable中,那么方法调用时又是怎么样的过程?

  如上文所言,MethodTable中包含了类型的元数据信息,类在加载时会在Loader Heap上创建这些信息,一个类型在内存中对应一份MethodTable,其中包含了所有的方法、静态字段和实现的接口信息等。对象实例的TypeHandle在实例创建时,将指向MethodTable开始位置的偏移处(默认偏移12Byte),通过对象实例调用某个方法时,CLR根据TypeHandle可以找到对应的MethodTable,进而可以定位到具体的方法,再通过JIT Compiler将IL指令编译为本地CPU指令,该指令将保存在一个动态内存中,然后在该内存地址上执行该方法,同时该CPU指令被保存起来用于下一次的执行。

  在MethodTable中,包含一个Method Slot Table,称为方法槽表,该表是一个基于方法实现的线性链表,并按照以下顺序排列:继承的虚方法,引入的虚方法,实例方法和静态方法。方法表在创建时,将按照继承层次向上搜索父类,直到System.Object类型,如果子类覆写了父类方法,则将会以子类方法覆盖父类虚方法。关于方法表的创建过程,可以参考[第十五回:继承本质论]中的描述。

  • 静态字段的内存分配和释放,又有何不同?

  静态字段也保存在方法表中,位于方法表的槽数组后,其生命周期为从创建到AppDomain卸载。因此一个类型无论创建多少个对象,其静态字段在内存中也只有一份。静态字段只能由静态构造函数进行初始化,静态构造函数确保在类型任何对象创建前,或者在任何静态字段或方法被引用前执行,其详细的执行顺序请参考相关讨论。

  3. 结论

  对象创建过程的了解,是从底层接触CLR运行机制的入口,也是认识.NET自动内存管理的关键。通过本文的详细论述,关于对象的创建、内存分配、初始化过程和方法调用等技术都会建立一个相对全面的理解,同时也清楚的把握了线程栈和托管堆的执行机制。

  对象总是有生有灭,本文简述其生,这是个伟大的开始。 
                            

                              © 2007 Anytao.com

NET技术[你必须知道的.NET] 第十九回:对象创建始末(下),转载需保留来源!

郑重声明:本文版权归原作者所有,转载文章仅为传播更多信息之目的,如作者信息标记有误,请第一时间联系我们修改或删除,多谢。