这篇我想解释的内容主要是关于类型、对象、线程栈以及托管堆在运行时候的相互关系。
我们都知道在编程语言进入某个方法时,大多数的做法都是在当前的线程栈当中将返回地址压入栈中,当方法运行完后再依次进行出栈直到最外层的调用。这样就能实现保存入口时的地址和程序进入方法前的状态。
.Net中也是这样实现的,现在有以下代码(摘自CLR via C# 第4版,第4章):
public class Employee{ public Int32 GetYearsEmployed(){...} public virtual String GenProgressReprot(){...}//虚方法 public static Employee LookUp(String name){...}//静态方法 } public class Manager:Employee{ public override String GenProgressReport(){...}//重写方法(虚方法) } void M1(){ String name="Jarvis"; M2(name); //do something return } void M2(String str){ Employee e; Int32 year; e = new Manager(); e = Employee.LookUp("ZhangSan"); year = e.GetYearsEmployed(); e.GenProgressReport(); }
1.执行M1方法
首先开辟1MB的栈空间
CLR在执行M1的时候会先对所有的局部变量进行初始化并压入栈中。(null/0,所以如果试图使用未显示初始化的对象就会爆掉。)
将M2函数的参数和返回地址压入栈中。
name |
str |
(返回地址) |
2.执行M2方法
为局部变量e、year分配空间。
name(string) |
str(string) |
(返回地址) |
e(Employee) |
year(int) |
3.CLR利用程序集中的元数据,创建M2内部引用的数据结构来表示类型本身。(这里需注意类型对象和类型的实例对象的区别)
1.数据结构包括:类型对象指针(指向对象的类型对象)、同步块索引、静态字段、方法表。
2.Manager和Employee的类型对象指向Type。Type指向自身。
类型对象本质也是对象,CLR创建这些对象时会初始化这些成员。CLR在运行时会立即创建一个特殊的System.Type类型对象。Employee和Manager类型对象都是该类型的实例。因此,他们的类型对象指针才会被初识化指向System.Type。
System.Object的GetType方法返回的就是指向类型对象的指针。CLR以此实现判断系统中任何的对象。
4.创建Manager对象的实例
到了这一步才开始真正构造对象的实例
Manager也会先初识化对象指针指向它的类型对象和同步索引块、实例字段(0/null)。然后再运行对象的构造函数,最后new操作符返回对象地址并保存至变量e中。
5.调用静态的LookUp方法
调用静态方法时,CLR会定位到静态方法的类型的类型对象(Employee类型对象)。然后找到对应的方法表中的记录项,对方法进行JIT编译(第一次执行该方法),再调用JIT生成的CPU指令。假设该方法到数据库中查找ZhangSan,然后返回一个全新的Manager对象,LookUp方法就会在堆上构造一个全新的Manager对象,用ZhangSan的数据库信息初始化它。然后返回该对象的地址保存在变量e中。然后旧的Manager对象会等待垃圾回收器进行回收释放。
6.调用GetYearsEmployed方法
调用实例方法时,JIT会找到与“发出调用的那个变量(e)的类型(employee)”对应的类型对象(Employee类型对象)。如果Employee没用定义正在调用的那个方法,那么JIT会回溯类层次结构,并在沿途的每个类型中查找该方法。之所以能够这样回溯是因为每个类型对象都有一个字段引用了他的基类型。
7.调用虚方法GetProgressReport()
调用虚方法,JIT会在方法中生成一些额外的代码。
1.这些代码首先检查发出调用的变量(e)。
2.找到发出调用的对象(Manager)。
3.通过对象的“类型对象指针”来找到实际类型,在方法表中查找引用的方法。
4.由于e应用的是Manager对象,所以调用的是Manager的GetProgressReport对象,如果Lookup方法返回的是Employee对象的话,这边调用的就是Employ的GetProgreessReport对象了。
8.方法运行完,出栈至返回地址,返回上一个方法继续执行。