[读书心得] .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 类型的实例对象