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

博客园CLR基础研究团队|CLR团队精品系列|Anytao技术博客

 

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

发布日期:2007.12.7 作者:Anytao
© 2007 Anytao.com ,原创作品,转贴请注明作者和出处。

 

本文将介绍以下内容:

  • 对象的创建过程
  • 内存分配分析
  • 内存布局研究

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

2.2 托管堆的内存分配机制

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

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

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

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

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

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

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

//@ 2007 Anytao.com 
//http://www.anytao.com
    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指令,其执行过程又可细分为以下几步:

aCLR按照其继承层次进行搜索,计算类型及其所有父类的字段,该搜索将一直递归到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字节的地址空间。

cCLR在当前AppDomain对应的托管堆上搜索,找到一个未使用的20字节的连续空间,并为其分配该内存地址。事实上,GC使用了非常高效的算法来满足该请求,NextObjPtr指针只需要向前推进20个字节,并清零原NextObjPtr指针和当前NextObjPtr指针之间的字节,然后返回原NextObjPtr指针地址即可,该地址正是新创建对象的托管堆地址,也就是aUser引用指向的实例地址。而此时的NextObjPtr仍指向下一个新建对象的位置。注意,栈的分配是向低地址扩展,而堆的分配是向高地址扩展。

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

在上述操作时,如果试图分配所需空间而发现内存不足时,GC将启动垃圾收集操作来回收垃圾对象所占的内存,我们将以后对此做详细的分析。

·       最后,调用对象构造器,进行对象初始化操作,完成创建过程。该构造过程,又可细分为以下几个环节:

   a)构造VIPUser类型的Type对象,主要包括静态字段、方法表、实现的接口等,并将其分配在上文提到托管堆的Loader Heap上。

b)初始化aUser的两个附加成员:TypeHandleSyncBlockIndex。将TypeHandle指针指向Loader Heap上的MethodTableCLR将根据TypeHandle来定位具体的Type;将SyncBlockIndex指针指向Synchronization Block的内存块,用于在多线程环境下对实例对象的同步操作。

c)调用VIPUser的构造器,进行实例字段的初始化。实例初始化时,会首先向上递归执行父类初始化,直到完成System.Object类型的初始化,然后再返回执行子类的初始化,直到执行VIPUser类为止。以本例而言,初始化过程为首先执行System.Object类,再执行User类,最后才是VIPUser类。最终,newobj分配的托管堆的内存地址,被传递给VIPUserthis参数,并将其引用传给栈上声明的aUser

上述过程,基本完成了一个引用类型创建、内存分配和初始化的整个流程,然而该过程只能看作是一个简化的描述,实际的执行过程更加复杂,涉及到一系列细化的过程和操作。对象创建并初始化之后,内存的布局,可以表示为:

    由上文的分析可知,在托管堆中增加新的实例对象,只是将
NextObjPtr指针增加一定的数值,再次新增的对象将分配在当前NextObjPtr指向的内存空间,因此在托管堆栈中,连续分配的对象在内存中一定是连续的,这种分配机制非常高效。

2.3 必要的补充

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

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

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

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

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

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

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

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

3. 结论

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

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

[祝福] 一个值得纪念的日子,一切快乐、平安、健康,这次专注,2008会更好。

参考文献

(USA)Joe Duffy, Professinal .NET Framework 2.0
(USA)Don Box, Essiential .NET
(MSDN)Hanu Kommalapati and Tom Christian, Drill Into .NET Framework Internals to See How the CLR Creates Runtime Objects, http://msdn.microsoft.com/msdnmag/issues/05/05/JITCompiler/default.aspx

温故知新

[开篇有益]
[第一回:恩怨情仇:is和as]
[第二回:对抽象编程:接口和抽象类]
[第三回:历史纠葛:特性和属性]
[第四回:后来居上:class和struct]
[第五回:深入浅出关键字---把new说透]
[第六回:深入浅出关键字---base和this]
[第七回:品味类型---从通用类型系统开始]
[第八回:品味类型---值类型与引用类型(上)-内存有理]
[第九回:品味类型---值类型与引用类型(中)-规则无边]
[第十回:品味类型---值类型与引用类型(下)-应用征途]
[第十一回:参数之惑---传递的艺术(上)]
[第十二回:参数之惑---传递的艺术(下)]
[第十三回:从Hello, world开始认识IL]
[第十四回:认识IL代码---从开始到现在]
[第十五回:继承本质论]
[第十六回:深入浅出关键字---using全接触]
[第十七回:貌合神离:覆写和重载]
[第十八回:对象创建始末(上)]

© 2007 Anytao.com

原创作品,转贴请注明作者和出处,留此信息。

本文以“现状”提供且没有任何担保,同时也没有授予任何权利。
This posting is provided "AS IS" with no warranties, and confers no rights.

posted @ 2007-12-07 08:55 Anytao 阅读(4967) 评论(46)  编辑 收藏 所属分类: 01 [你必须知道的.NET]

  回复  引用  查看    
#1楼 2007-12-07 09:17 | jillzhang      
终于出下了,站个沙发,再看
  回复  引用  查看    
#2楼 [楼主]2007-12-07 09:23 | Anytao      
@jillzhang
呵呵,等久了,最近事儿多,好在上次骨架已经有了:-)
  回复  引用  查看    
#3楼 2007-12-07 09:30 | Jack Niu      
今天才发现还有这样一个系列,一定抓紧时间赶紧看完!
  回复  引用    
#4楼 2007-12-07 09:31 | gj [未注册用户]
方法表:
方法槽表应该先是基类的方法成员(一直到OBJECT),然后是自己的,自己里的,顺序是继承的虚方法,引入的虚方法,实例方法和静态方法。
在方法槽表上面的应该还又委托,这点应该要提下了。
方法槽下面才是静态字段,接口吧。
  回复  引用  查看    
#5楼 [楼主]2007-12-07 09:38 | Anytao      
@Jack Niu
欢迎欢迎:-)
  回复  引用    
#6楼 2007-12-07 09:43 | gj [未注册用户]
希望楼主后面的系列分析下在方法表中接口,虚方法的多态实现怎么样体现,还有委托谢谢!
  回复  引用  查看    
#7楼 [楼主]2007-12-07 09:43 | Anytao      
@gj
文中基本都有提到了,漏了委托,不过因为重点并非分析方法表的详细布局,所有有很多忽略:-)
  回复  引用  查看    
#8楼 [楼主]2007-12-07 09:45 | Anytao      
@gj
第十五回:继承本质论, http://www.cnblogs.com/anytao/archive/2007/09/10/must_net_15.html
中有比较详细的论述,不过没有从更底层的分配过程来详述。
关于委托,系列后续肯定会有,谢谢你的关注。
  回复  引用    
#9楼 2007-12-07 09:48 | gj [未注册用户]
@Anytao
感谢你的系列,我经常看看,就是对接口,虚方法的多态实现,JIT具体是怎么实现的,怎么通过方法表的,还不是很清晰。要是详细的更好。
  回复  引用  查看    
#10楼 2007-12-07 09:52 | jillzhang      
博主的图用什么工具画的呢?visio么?
  回复  引用  查看    
#11楼 [楼主]2007-12-07 09:58 | Anytao      
@jillzhang
正是Visio 2003,:-)
  回复  引用  查看    
#12楼 [楼主]2007-12-07 10:00 | Anytao      
@gj
有时间一定再深入一下,我也喜欢刨根问底,我想找到合适的思路和方式来分析负责的过程,需要点思考和准备,所以以后会有这方面的分析:-)
  回复  引用  查看    
#13楼 2007-12-07 10:34 | DotPro.Net      
谢谢LZ!关注中ing
  回复  引用  查看    
#14楼 [楼主]2007-12-07 10:37 | Anytao      
@DotPro.Net
:-)
  回复  引用    
#15楼 2007-12-07 11:01 | tran收到是 [未注册用户]
对接口,虚方法的多态实现,JIT具体是怎么实现的,怎么通过方法表的

建议看一下 essential .net
  回复  引用  查看    
#16楼 [楼主]2007-12-07 11:07 | Anytao      
@tran收到是
正是,不错的提醒,:-)
  回复  引用  查看    
#17楼 2007-12-07 15:11 | 坐断东南 笑煞之!!      
又见一期好文章。。

呵呵。。
  回复  引用  查看    
#18楼 2007-12-07 15:30 | mythzz      
最近我也在看.net本质 那本书,看得有点郁闷,准备看 第二遍,看到你的文章,使我加深了理解啊,真的太感谢了!
支持 博主啊
  回复  引用  查看    
#19楼 [楼主]2007-12-07 15:58 | Anytao      
@坐断东南 笑煞之!!
呵呵,你今天来晚了,:-)
  回复  引用  查看    
#20楼 [楼主]2007-12-07 16:00 | Anytao      
@mythzz
表示感谢,到时可得支持:-)
  回复  引用    
#21楼 2007-12-07 23:03 | xiongjianwen [未注册用户]
赞一个!!!
比Jeffrey Richter 写的还好~~ 哈哈~~~
  回复  引用  查看    
#22楼 [楼主]2007-12-07 23:13 | Anytao      
@xiongjianwen
哈哈,有始以来的最高评价,虽然不客观,但是很高兴。
Jeffrey Richter可是我的引路人。
  回复  引用  查看    
#23楼 2007-12-08 16:56 | 雨恨云愁      
写的很好。
把我头脑中杂乱的知识梳理了一遍。
  回复  引用  查看    
#24楼 [楼主]2007-12-08 17:28 | Anytao      
@雨恨云愁
这个系列正式这个目的,能够达到这个目标就够了:-)
  回复  引用    
#25楼 2007-12-10 10:24 | 冰之印记 [未注册用户]
赞一个!!!
比Jeffrey Richter 写的还好~~ 哈哈~~~
========================
这话并不为过,上次博乐园的服务器坏了 没能及时回复,
我也在看李建中翻译的那本书,的确比书里面讲的更详细一点,应该说看完您的文章之后,理的更加清楚了!
关注博主的文章,感谢分享!
  回复  引用  查看    
#26楼 [楼主]2007-12-10 11:25 | Anytao      
@冰之印记
呵呵,谢谢你的肯定。
Jeffrey Richter的书,确实很精彩,李建忠老师的翻译也非常独到。《.NET框架设计》是一本宏图巨著,可能很多时候无法深入细节,所以我就将这些东西作以补充、稍加深入,欢迎你的讨论。
相信后续系列会更好:-)
  回复  引用    
#27楼 2007-12-11 11:02 | redfox888888 [未注册用户]
楼主,我一直有个困惑,将IL代码转换成CPU指令后,这些指令保存在哪里呢?在内存中么?还是在硬盘的某个文件中???

请楼主替我解除疑惑!

谢谢.
  回复  引用  查看    
#28楼 [楼主]2007-12-11 11:35 | Anytao      
@redfox888888
JIT将中间语言编译为x86机器码,保存在内存中,例如方法调用生成机器码可能在随后的调用中还要用到,下次调用相同的方法会直接执行内存中的机器代码,而不会再次编译IL代码,这是编译器的性能优化策略。
  回复  引用    
#29楼 2007-12-12 10:28 | ccydn [未注册用户]
写的不错。顶一个。期待楼主更好的文章
  回复  引用  查看    
#30楼 [楼主]2007-12-12 11:13 | Anytao      
@ccydn
嘿嘿,再接再励,谢谢支持。
  回复  引用    
#31楼 2007-12-13 23:44 | 嘟嘟嘟嘟 [未注册用户]
这两天一下班就研读 Anytao 你的系列大作,下一篇什么时候出呢?期待啊!
  回复  引用  查看    
#32楼 [楼主]2007-12-14 00:01 | Anytao      
@嘟嘟嘟嘟
最近实在太忙了,手上一堆事情,可能得休战一些天了,不过有功夫肯定会第一时间发布的,谢谢关注:-)
  回复  引用    
#33楼 2007-12-15 09:39 | lihaoware [未注册用户]
看了楼主这一系列的文章受益匪浅,向楼主表示敬意!

在本文中我对楼主关于“VIPUser类型实例”占内存大小的计算有疑问:

文中“类型VIPUser需要的字节总数为11Byte,具体计算为:VIPUser类型本身字段isVip(bool型)为1Byte;父类User类型的字段id(Int32型)为4Byte,字段user(UserInfo型)为6Byte。

我认为:UserInfo类型实例占6byte,而VIPUser类型父类User中的user字段只是UserInfo类型的引用,其内存分配应为4byte(文中上下文指的32位机器),而不是6byte。User类型实例占内存为6byte,VIPUser类型实例占内存为17byte
  回复  引用  查看    
#34楼 [楼主]2007-12-15 12:05 | Anytao      
@lihaoware
的确有失严谨,已经做了检查,在user字段的判断部分分析有误,正确的应该是:
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字节的地址空间。


由于网络问题,暂时不能修改,我将择日尽快修改,感谢你的指正:-)

  回复  引用  查看    
#35楼 [楼主]2007-12-27 09:06 | Anytao      
@Anytao
已做修订,谢谢你的指正:-)
  回复  引用  查看    
#36楼 2007-12-30 09:02 | 飞哥      
涛哥,你太有才了。赞一个!
  回复  引用  查看    
#37楼 [楼主]2007-12-31 22:37 | Anytao      
@飞哥
呵呵,谢谢,新年快乐:-)
  回复  引用    
#38楼 2008-01-08 14:59 | redfox88888 [未注册用户]
大哥,什么时候讲对象的灭啊???都等了这么久了,还没见你写.
  回复  引用  查看    
#39楼 [楼主]2008-01-08 20:25 | Anytao      
@redfox88888
最近实在太忙了,好多的事情堆在一起了,有时间的时候一定完成后续的部分,希望大家理解。。。
  回复  引用    
#40楼 2008-02-21 18:08 | cnfixit [未注册用户]
博主啊
二月底啦,快三月了
下一篇啥时候出啊
期待ing
  回复  引用  查看    
#41楼 [楼主]2008-02-21 22:01 | Anytao      
@cnfixit
近期忙于新书筹备,实在太忙了,不过近期就有功夫回到我喜欢的事情了,一有时间即可完成下篇,其实我也很着急:-)
  回复  引用  查看    
#42楼 2008-08-11 10:22 | 蜘蛛液      
今日有幸拜读先生的本章大作,实在有些问题没有搞明白,希望指点。
1. 关于 @lihaoware 网友的提议,楼主还没有修正过来哦。我认同此观点。
2. 方法保存在Loader Heap的MethodTable中,那么方法调用时又是怎么样的过程?
楼主详细分析了对象方法路径的搜索,obj-->TypeHandle-->MethodTable-->method...,但我不明白,在方法调用的内部,是如何访问到当前对象的字段呢?我知道在C++中是在成员方法的参数列表后面添加上 this值,现在C#中也有this,但不知道它是以什么机制访问到对象的字段。应当不会也在参数后多加一个参数的。
  回复  引用    
#43楼 2008-09-29 13:19 | macou_1005 [未注册用户]
VIPUser aUser;
VIPUser bUser;
aUser=new VIPUser();
bUser=newVIPUser();

以上操作在内存中会存在两份方法表吗?

标题  
姓名  
主页
Email (博主才能看到) 
验证码 *  看不清,换一张 [登录][注册]
内容(请不要发表任何与政治相关的内容)  
  登录  使用高级评论  新用户注册  返回页首  恢复上次提交      
该文被作者在 2008-09-22 14:04 编辑过
"五向定位"职业成长路线公开课(上海、南京、大连)
Google站内搜索


相关链接: