[读书心得] .NET中 类型,对象,线程栈,托管堆在运行时的关系

.NET中 类型,对象,线程栈,托管堆 在运行时的关系

The Relationship at Run Time between Types,Objects,A Thread's Stack,and The Managed Heap for .NET

by 唐小崇

http://www.cnblogs.com/tangchong

  .NET中的类型,无论是值类型或引用类型都是继承自Object的类。这点跟Java类似,但与C/C++有很大不同。既然值类型与引用类型都是类,那它们的没有什么不同的地方。而最值得关注的不同就是:值类型对象的值直接存储在线程栈中,引用类型对象的值存放在托管堆中,它的引用存放在线程栈中。本篇博文就谈谈CLR的线程栈,托管堆策略。

  当我们执行一个.NET程序, CLR  会加载一个新的进程,这个进程可能会(按照程序的编写需求)包含多个线程。当一个线程被创建,它会申请一个 1MB大小栈(stack)。这个栈是以高位到地位保存的。线程栈是用来保存本地变量,参数,返回地址的,稍后会有详细解释。同时CLR会创建一个托管堆。(由于内容太多,先从线程栈开始讲起,稍后再讲托管堆)

现在,让我们通过两个例子方法M1,M2来了解线程栈与类型对象的关系:

我们先看看线程栈的样子。图1为一个线程栈的示例,我们假设该程序已经执行了一部分代码,并即将执行M1方法。

图1:线程栈示例

线程栈有以下作用:

  ·保存向某个方法传递的参数(passing arguments);

  ·保存当前执行的方法内的本地变量(local variales);

  ·保存当前执行方法的返回地址(return address);

所有的方法都会包含一些 序幕代码(prologue code)和 收场代码(epilogue code) ,序幕代码用来初始化一个方法,这些代码把本地变量,参数,返回地址压入线程栈中。而收场代码会在函数执行完毕后会清理给该函数分配的空间,并将指令指针(CPU's instruction pointer)指向返回地址(即指向该函数的调用者),从而释放线程栈空间。

当我们开始执行M1,CLR将 本地变量name、向M2传递的参数s 和 返回地址 压入线程栈中,如图2所示:

 图2:将本地变量name、向M2传递的参数s和返回地址压入线程栈中

接着我们执行到M2(name);CLR开始调用M2方法,这时序幕代码会在线程栈里分配两个本地变量 length,tally,如图3所示。当M2方法执行完毕后,收场代码清理M2分配的空间,并将指令指针指向返回地址(即M1)。此时,我们的线程栈又回到图2所示的状态。

 

图3:M2方法开始执行时线程栈的情况

以上,就是整个线程栈的执行情况。

我们接着来看看CLR中的托管堆。说到托管堆,就不得不提引用类型。下面我们列两个示例类,Employee和它的子类Manager:

internal class Employee { 
public Int32 GetYearsEmployed() { ... } 
public virtual String GetProgressReport() { ... } 
public static Employee Lookup(String name) { ... } 
} 

internal sealed class Manager : Employee { 
public override String GetProgressReport() { ... } 

如图4所示:我们现在加入一个新方法M3。当我们的程序开始执行,CLR 会初始化线程栈和托管堆。

首先JIT编译器将M3方法的中间代码(IL)JIT编译为本地指令(native CPU instructions)。

然后CLR检测M3引用的所有类型(本例中为Employee,Int32,Manager,String),这时CLR会确保提供这些类型的程序集已经被加载(否则,会报错)。

最后CLR提取有关这些类型信息创建一些数据结构(Type Object)来表示类型本身。也就是说,CLR会先创建 Type Object,然后通过它来创建类的实例对象。

图4:当M3方法被调用。CLR在托管堆中创建对应的类的类型对象(Type Object)

在创建好的类型对象 Type Object(并不是类的实例对象)中包括以下成员

  类型对象指针(Type object ptr)

  同步块索引(Sync block index)

  静态成员(Statoc fieds)

  方法列表(method table)

紧接着,当CLR确认M3所需求的所有的类型对象(Type Object)都被创建,则开始执行M3的本地代码。如图5所示,序幕代码在线程栈中创建本地变量并将它们初始化为null或者0。

图5 序幕代码在线程栈中创建本地变量

程序开始执行e = new Manager();

如图6所示,这行代码指示CLR在托管堆中创建一个Manager类型的实例对象(instance)。该实例对象包含: 类型对象指针、 同步块索引以及 该类型中的定义成员(包括它的父类成员,在本例中父类为Employee,Object)。

然后,CLR会自动将该实例对象(Manager Object)的 类型对象指针(Type object ptr)指向相应的类型对象(Manager Type Object),此外,CLR会初始化同步块索引和所有成员为0并调用构造器(构造函数)。

最后new 操作符将创建好的实例对象在托管堆中的地址 返回并赋值给线程栈中的引用类型变量e。

图6:创建并初始化一个Manager实例对象

下面我们看看3种不同的方法:静态方法,非虚方法,虚方法的执行情况。

下一行代码是e = Employee.Lookup("Joe"); 这里调用了Employee的静态方法Lookup()。

如图7所示,当调用一个静态方法,首先JIT编译器会定位到该方法对应的类型Type Object对象(本例中为Employee Type Object)。然后将该Type Object函数列表中的对应方法(本例中是Lookup)JIT编译,并执行。

我们假设Joe存在并且是一个经理,则Lookup函数会在托管堆中创建一个Manager实例对象,并用Joe初始化它。最后将这个Manager实例对象的地址返回,赋值给e。

图7:静态函数Lookup被调用,创建并用joe初始化一个Manager 对象,并赋值给e

继续执行下一行代码:year = e.GetYearsEmployed();

如图8所示:当我们调用一个非虚函数(nonvirtual instance method),JIT编译器会定位到 调用者的类型(e的类型为Employee)对应Type Object中(本例中为Employee Type Object)。在该Type Object中的方法列表中,查找对应方法(如果没找到,会向其父类寻找直到Object为止)。JIT编译该方法,并执行。我们不妨假设joe已经工作5年了。则Employee 的 GetYearsEmployed方法返回5,并赋值给线程栈中的year变量。

图8:调用Employee的非虚函数 GetYearsEmployed。

接下来一行代码是e.GetProgressReport();该方法是一个虚方法(virtual instance method)。

调用一个虚方法前,JIT编译器会额外的执行一些代码。这些代码会先查找到 调用该方法的变量(e)所指向的实例对象(在本例中,即为用Joe初始化的Manager实例对象)。接下来,检查该对象中的类型对象指针,以找到对象的真正类型type object(本例中为Manager Type Object)。最后JIT编译该type object方法列表中的对应方法,并执行。

如图9所示,JIT编译并执行的是 Manager Type Object中的GetProgressReprot方法,而不是Employee Type Object中的。

图9:调用虚函数GetProgerssReport(),实际类型Manager的type Object中的方法被执行

至此,示例代码结束。我们讨论了调用静态方法,非虚函数,虚函数的三种情况。但我们还有一点没有完成。

我们会注意到在Type Object中也有Type object ptr,这是因为这些Type Object也是一个“类型”的实例对象。它们的类型比较特殊,叫做System.Type,定义在MSCorLib.dll中。当CLR开始执行一个进程,它会先为System.Type创建一个Type Oject,称为Type Type Object。如图10所示,本例的 Employee Type Object,Manager Type Object都是Type Type Object的“实例对象”。最后,Type Type Object 本身也是一个对象,它的Type object ptr 指向自身。这就是.NET 万物皆对象的思想。

图10:Emloyee和Manager的type objects 是 System.Type 类型的实例对象

 

posted on 2013-08-05 18:05  唐小崇  阅读(1858)  评论(8编辑  收藏  举报

导航