类型、对象、线程栈和托管堆在运行时的相互关

作者:编程技术

1.clr计算出类型的所有实例字段的字节和所有基类型的实例字段的字节长度,创建类型对象指针和同步块索引(也计算在字节长度内)

分分快三计划 1

 

 c.详细解释

分分快三计划 2

   分分快三计划 3

  4.e.M1();找到e对象类型对应的对象类型Manager(没有,回溯到Employee中找),在方法列表中找到对应的方法,编译执行(能够向上回溯是因为在派生类中有指向基类的引用)

      注意,e不再引用第一个Manager对象。事实上,由于没有变量引用这个对象,所以它是将来进行垃圾回收时的主要目标。垃圾回收机制会自动回收(释放)这个对象占用的内存。

      M3的下一行代码调用Employee 的非虚实例方法GetYearsEmployed。调用一个非虚实例方法时,JIT编译器会找到与“发出调用的那个变量(e)的类型(Employee)”对应的类型对象(Employee类型对象)。本例中,变量e被定义成一个Employee。如果Employee类型没有定义正在调用的那个方法,JIT编译器会回溯类层次结构(一直回溯到Object),并在沿途的每个类型中查找该方法。之所以能这样回溯,是因为每个类型对象都有一个字段引用了它的基类型,这个信息在图中没有显示。

      然后,JIT 编译器在类型对象的方法表中查找引用了被调用方法的记录项,对方法进行JIT 编译(如果需要的话),再调用JIT编译的代码。就本例来说,假定Employee的GetYearsEmployed方法返回5,因为Joe已被公司雇用了5年。这个整数保存在局部变量year中。这个操作的结果如图4-11所示。

  if ( o is Employe ){
       Employee e = (Employee) o;
  }

  public virtual string M2(){.....};

分分快三计划 4

  new 执行了所有的操作后,会返回执行新建对象的一个引用。在本例中,这个引用会保存到变量e中,具有Employee类型。

 

分分快三计划 5

  CLR要求所有对象都是用new操作符来创建。比如  

 b.运行关系图

假定有以下两个类定义:

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() { ... }

}

      我们得windows进程已经启动,CLR已加载到其中,托管堆已初始化,而且已创建一个线程(连同它的1MB的栈空间)。该线程已执行了一些代码,现在马上要调用M3的方法。下图展示了目前的状况。M3方法包含的代码演示了CLR是如何工作的,平时不会这么写,因为它们没有做什么真正有用的事情。

  4. M2的方法开始执行时,它的"序幕"代码就是在线程栈中为局部变量length和tally分配内存。如图4-5所示。

  3 Employee.M3("zhangsan"); 第一个对象将被垃圾回收器回收。他会找到调用它的类型,然后去类型对象的方法列表中找到这个方法,

本节将解释类型、对象、线程栈和托管堆在运行时的相互关系。此外,还将解释调用静态方法、实例方法和虚方法的区别。

  4)调用类型的实例构造器,向其传入对new的调用中指定的任何实参(本例中是"ConstructorParam1")。大多数编译器都在构造器中自动生成代码来调用一个基类的构造器。每个类型的构造器在被调用时,都要负责初始化这个类型定义的实例字段。最后调用的是System.Object的构造器,该构造器只是简单的返回,不会做其它任何事情。

如图:

 

  4. 先前提过,堆上的所有对象上都包含两个额外的成员:"类型对象指针"和"同步块索引"。如图4-7所示,Employee和Manager类型对象都有这两个成员。定义一个类型时,可以在类型的内部定义静态数据字段。为这些静态字段数据提供支持的字节是在类型对象自身中分配到的。在每个类型对象中,都包含一个方法表。在方法表中,类型中定义的每个方法都有一个对应的记录项。由于Employee有3个方法就有3个记录项,Manager只有一个方法,也就只有一个记录项。

分分快三计划 6

      M3的下一行代码调用Employee的静态方法Lookup。调用一个静态方法时,CLR会定位与定义静态方法的类型对应的类型对象。然后,JIT编译器在类型对象的方法表中查找与被调用的方法对应的记录项,对方法进行JIT编译(如果需要的话),再调用JIT编译的代码。就本例来说,假定Employee的Lookup方法要查询一个数据库来查找Joe。另外,假定数据库指出Joe 是公司的一名经理,所以在内部,Lookup 方法在堆上构造一个新的Manager对象,用Joe的信息初始化它,然后返回该对象的地址。这个地址保存到局部变量e中。这个操作的结果如图4-10所示。

  7. M3的下一行代码调用Employee的静态方法Lookup。调用一个静态方法时,CLR会定位到与定义静态方法的类型对应的类型对象。然后,JIT编译器在类型对象的方法表中查找被调用的方法对应的记录项,对该方法进行JIT编译(如果需要的话),再调用JIT编译后的代码。就本例,假定Enployee的Lookup方法要查询数据中的Joe。另外,假定数据库中指出Joe是为Manager,所以在内部,Lookup方法在堆上构造一个新的Manager对象,用Joe的信息初始化它,然后返回该对象的地址。这个地址保存在局部变量e中。如图4-10所示。值得注意的是,e不再引用第一个Manager对象。事实上,由于没有变量引用第一个Manager对象,所以它是将来进行垃圾回收时的主要目标。

2.运行时关系

      当JIT编译器将M3的IL代码转换成本地CPU指令时,会注意到M3内部引用的所有类型:Employee,Int32,Manager以及String(因为“Joe”)。这个时候,CLR要确保定义了这些类型的所有程序集都已加载。然后,利用程序集的元数据,CLR提取与这些类型有关的信息,并创建一些数据结构来表示类型本身。下图展示了为Employee和Manager类型对象使用的数据结构。由于这个线程在调用M3之前已经执行了一些代码,所以不妨假定Int32和String类型对象已经创建好了,所以图中不显示它们。

  9. M3的下一行代码调用Empolyee的虚实例方法GenProgressReport。调用一个虚实例方法时,JIT编译器要在方法中生成一些额外代码;方法每次调用时,都会执行这些代码。这些代码首先检查发出调用的变量,然后跟随地址来到发出调用的对象。在本例中,变量e引用的是代表"Joe"的一个Manager对象。然后,代码检查对象内出的"类型对象指针"成员,这个成员指向对象的实际类型。然后,代码在类型对象的方法表中查找引用了被调用方法的记录项,对方法进行JIT编译(如果需要的话),再调用JIT编译后的代码。在本例中,由于e引用了一个Manager对象,所以会调用Manager的GenProgressReport实现。如图4-12所示。

{

      然后,M3执行它的代码来构造一个Manager对象。这造成在托管堆中创建Manager类型的一个实例(也就是一个Manager对象),如图4-9所示。可以看出,和所有对象一样,Manager对象也有一个类型对象指针和同步块索引。该对象还包含必要的字节来容纳Manager类型定义的所有实例数据字段,以及容纳由Manager的任何基类(本例就是Employee和Object)定义的所有实例字段。任何时候在堆上新建一个对象,CLR都会自动初始化内部类型对象指针成员,让它引用与对象对应的类型对象(本例就是Manager类型对象)。此外,CLR会先初始化同步块索引,并将对象的所有实例字段设为null或0(零),再调用类型的构造器(它本质上是可能修改某些实例数据字段的一个方法)。new操作符会返回Manager对象的内存地址,该地址保存在变量e中(e在线程栈上)。

  总结:

window的一个进程加载clr。该进程可能含有多个线程,线程创建的时候会分配1MB的栈空间。

      注意,如果Employee的Lookup方法发现Joe只是一个Employee,而不是一个Manager,Lookup会在内部构造一个Employee对象,它的类型对象指针将引用Employee类型对象。这样一来,最终执行的就是Employee的GenProgressReport实现,而不是Manager的GenProgressReport实现。

      至此,我们已经讨论了源代码、IL和JIT编译的代码之间的关系。还讨论了线程栈、实参、局部变量以及这些实参和变量如何引用托管堆上的对象。我们还知道对象中包含一个指针,它指向对象的类型对象(类型对象中包含静态字段和方法表)。我们还讨论了JIT编译器如何决定静态方法、非虚实例方法以及虚实例方法的调用方式。理解这一切之后,可以深刻地认识CLR的工作方式。以后在建构、设计和实现类型、组件以及应用程序时,这些知识会带来很大帮助。在结束本章之前,让我们更深入地探讨一下CLR内部发生的事情。

      注意,Employee和Manager类型对象都包含“类型对象指针”成员。这是由于类型对象本质上也是对象。CLR创建类型对象时,必须初始化这些成员。初始化成什么呢?CLR开始在一个进程中运行时,会立即为MSCorLib.dll中定义的System.Type类型创建一个特殊的类型对象。Employee和Manager类型对象都是该类型的“实例”。因此,它们的类型对象指针成员会初始化成对System.Type类型对象的引用,如图4-13所示。

  以下是new操作符所做的事情:

  public int32 M1(){.....};

      当然,System.Type类型对象本身也是一个对象,内部也有一个“类型对象指针”成员。那么这个指针指向的是什么呢?它指向它本身,因为System.Type类型对象本身是一个类型对象的“实例”。现在,我们总算理解了CLR的整个类型系统及其工作方式。顺便说一句,System.Object的GetType方法返回的是存储在指定对象的“类型对象指针”成员中的地址。换言之,GetType方法返回的是指向对象的类型对象的一个指针。这样一来,就可以判断系统中任何对象(包括类型对象本身)的真实类型。

  注意:上面提到过"类型对象指针",类型对象不是类型的对象/实例,这两者是有区别的。

a.首先介绍下new 关键字的执行的时候会执行什么

分分快三计划 7

  3. 然后,M1调用M2的方法,将局部变量name作为一个实参来传递。这造成name局部变量中的地址被压入栈(参见图4-4)。在M2方法内部,将使用名为s的参数变量来标识栈位置(有的CPU架构会通过寄存器来传递实参,以提高性能)。另外,调用一个方法时,还会将一个"返回地址"压入栈中。被调用的方法在结束后,应该返回到这个位置(同样参见图4-4)。

  return;    //methed2执行完后,指针指向线程栈的返回地址,method2的栈帧展开

分分快三计划 8

   public static void Main() {
      // 不需要转型
      Object o = new Employee();

      // 需要进行强制类型转换
      Employee e = (Employee) o;
   }

  e=new Manager();

分分快三计划 9

 

    然后JIT进行编译,然后执行。

      让我们花点时间来讨论一下这些类型对象。本章前面讲过,堆上的所有对象都包含两个额外的成员:类型对象指针(type object pointer)和同步块索引(sync block index)。如图所示,Employee和Manager类型对象都有这两个成员。定义一个类型时,可以在类型的内部定义静态数据字段。为这些静态数据字段提供支援的字节是在类型对象自身中分配的。在每个类型对象中,最后都包含一个方法表。在方法表中,类型中定义的每个方法都有一个对应的记录项。我们已经在第一章讨论过这个方法表。由于Employee类型定义了3个方法,所以Employee的方法表中有3个记录项。Manager类型只定义了一个方法,所以Manager的方法表中只有1个记录项。

      现在,当CLR确定方法需要的所有类型对象都已创建,而且M3的代码已经编译之后,就允许线程开始执行M3的本地代码。M3的“序幕”代码执行时,必须在线程栈中为局部变量分配内存,如图4-8所示。顺便说一句,作为方法的“序幕”代码的一部分,CLR会自动将所有局部变量初始化为null或0(零)。然而,如果试图从一个尚未显式初始化的局部变量读取数据,C#会报告错误消息:使用了未赋值的局部变量。

  CLR要求所有类型最终都要从System.Object派生。也就是所,下面的两个定义是完全相同的,

  e.M2();

分分快三计划 10

  6. 然后,M3执行它的代码来构造一个Manager对象。这就会在托管堆中创建Manager类型的一个实例(也就是Manager对象)。如4-9所示。和所有对象一样,Manager对象也有一个"类型对象指针"和"同步块索引"。该对象还包含必要的字节来容纳Manager类型定义的所有实例数据字段,以及容纳由Manager的任何基类(Employee和Object)定义的所有实例字段。任何时候在堆上新建一个对象,CLR都会自动初始化内部"类型对象指针",让它引用(或指向)与对象对应的类型对象(本例就是Manager类型对象)。此外,CLR会先初始化"同步块索引",并将对象的所有实例字段设为nll或为零(0),在调用类型的构造器(它本质上是可能修改某些实例字段的一个方法)。new操作符会返回Manager对象的内存地址,该内存地址保存在变量e中(e在线程栈上)。

       int32 tally;

      M3的下一行代码调用Employee的虚实例方法GenProgressReport。调用一个虚实例方法时,JIT 编译器要在方法中生成一些额外的代码;方法每次调用时,都会执行这些代码。这些代码首先检查发出调用的变量,然后跟随地址来到发出调用的对象。在本例中,变量e引用的是代表“Joe”的一个Manager对象。然后,代码检查对象内部的“类型对象指针”成员,这个成员指向对象的实际类型。然后,代码在类型对象的方法表中查找引用了被调用方法的记录项,对方法进行JIT编译(如果需要的话),再调用JIT编译过的代码。就本例来说,由于e引用一个Manager对象,所以会调用Manager的GenProgressReport实现。这个操作的结果如图4-12所示。

  分分快三计划 11

 

  命名空间和程序集不一定是相关的,也就是说它们之间没有必然联系。

4.调用类型的实例构造器。

  分分快三计划 12

3.初始化类型对象指针(指向类型对象)和同步块索引

  分分快三计划 13

void M4()

GetType

       return;    

  2. 我们的Windows进程已启动,CLR已加载到其中,托管堆已初始化,而且已创建一个线程(连同它的1MB的栈空间)。该线程已执行了一些代码,现在马上就要调用M3的方法。图4-6展示了目前的状况。M3方法包含的代码演示了CLR是如何工作的。

}

  

}

  

  2.e=new Manager();会在托管推上分配Manager和所有基类的实例字段字节大小,初始化类型对象指针,指向Manager类型对象。

  分分快三计划 14

1、线程栈

  分分快三计划 15

  public static Employee M3(string name){.....};

  3. 当JIT编译器将M3的IL代码转换成为本地CPU指令时,会注意到M3的内部引用的所有类型:Employee、Int32、Manager以及String(因为"Joe")。这个时候,CLR要确保定义了这些类型的所有程序集都已经加载。然后,利用这些程序集的元数据,CLR提取与这些类型有关的信息,并创建一些数据结构表示类型本身。图4-7展示了为Employee和Manager类型对象使用的数据结构。由于这个线程在调用M3之前已经执行了一些代码,所有不妨假定Int32和String类型对象已经创建好了,所以图中没有显示它们。

  int32 age;

  5. 现在,当CLR确定方法需要的所有类型对象都已经创建了,而且M3的代码也已经编译好了,就允许线程开始执行M3的本地代码。M3的"序幕"代码执行时,必须在线程栈中为局部变量分配内存,如4-8所示。作为方法的"序幕"代码的一部分,CLR会自定将所有局部变量初始化为null或零(0)。

internal class Employee

  1. 图4-2展示了已加载了CLR的一个Windows进程。在这个进程中,可能存在多个线程。一个线程创建时,会分配到一个1MB大小的栈。这个栈的空间用于向方法传递实参,并用于方法内部定义的局部变量。图4-2展示了一个线程的栈内存(右侧)。栈是从高地址向低地址构建的。在图中,线程已执行了一些代码,现在,假定线程开始执行的代码要调用M1方法了。

       1.M4运行的时候 先在线程栈 压入e和age两个局部变量

  ----------------------------------------------------------------------------------

 

  在C#语言中进行类型转换的另一种方式是使用is操作符。is操作符检查一个对象是否兼容指定的类型,并返回一个Boolean值(true和false)。注意,is操作符是不会返回异常信息的。

 void Method()

使之看起来类似于图4-3。之后,M1将继续执行在M2调用之后的代码,M1的栈帧将准确反映M1需要的状态。

void Method2(string s)

   命名空间(namespace)用于对相关的类型进行逻辑分组,开发人员使用命名空间来方便的定位一个类型。

  e=Employee.M3("zhangsan");

Employee e = new Employee("ConstructorParam1");

现有如下2个类型

  在这段代码中,CLR实际是会检查两次对象的类型。is操作符首先核实o是否兼容Employee类型。如果是,在if内部,CLR还会再次核实o是否引用一个Employee。CLR的类型检查增强的安全性,但无疑也会对性能造成一定影响。

  int32 length=s.Length;

internal class Employee {
    public               int32         GetYearsEmployed()       { ... }
    public    virtual    String        GenProgressReport()      { ... }
    public    static     Employee      Lookup(String name)      { ... }     
}
internal sealed class Manager : Employee {  
    public    override   String         GenProgressReport()    { ... }
}     

  Employee e;

  分分快三计划 16

{

  6. 最后,M1会返回到它的调用者。同样的是通过CPU的指令指针设置成返回地址来实现的(这个返回地址在图中未显示,但它应该刚好在栈中的name实参上方),而且M1的栈帧会展开,使之看起来类似于图4-2。之后,调用了M1的方法会继续执行在M1之后的代码,那个方法的栈帧将准确反映它需要的状态。

}

  分分快三计划 17

{

  System.Object提供了如下所示的公共实例方法。  

  public override string M2(){.....};

  当然,System.Type类型对象本身也是一个对象,内部也有一个"类型对象指针"成员。那么这个指针指向的是什么呢?它指向它本身,因为System.Type类型对象本身就是一个类型对象的"实例"。

{

 

  5.e.M2()找到e对象的的对象类型(Manager),调用Manager类型对象方法列表中的M2 而不是Employee中的。

   分分快三计划 18

       Method2(name);  //1参数变量s 被压入栈,s引用name的地址    2.返回地址被压入栈,方法执行完(method2的 return)指针指向此返回地址

  分分快三计划 19

 

   

{

  2)它从托管堆中分配指定类型要求的字节数,从而分配对象的内存,分配的所有字节都设为零(0)。

2.在托管堆上分配第一步长度的空间

  由于所有类型最终都是从System.Object派生的,所以可以保证每个类型的每个对象都有一组最基本的方法。

分分快三计划 20

  

准备:window进程已经开启,clr已经加载到进程里面,托管堆已经初始化,线程栈也已经被创建(连同它的1MB的栈空间)

  1. 假定现在有以下两个类的定义:

}

  注意,在Employee和Manager类型对象都包含"类型对象指针"成员。这是由于类型对象本质也是对象。CLR创建类型对象时,必须初始化这些成员。初始化成什么呢?CLR开始在一个进程中运行时,会立即为MSCOrLib.dll中定义的System.Type类型创建一个特殊的类型对象。Employee和Manager类型对象都是该类型的"实例".因此,它们的类型对象指针成员会初始化成对System.Type类型对象的引用。如图4-13。

  string name="zhangsan";  //name 被放入栈里面

  1)它计算类型及其所有基类型(一直到System.Object)中定义的所有实例需要的字节数。堆上的每个对象都需要一些额外的开销成员——"类型对象指针(type object pointer)"和"同步块索引"(sync block index)。这些成员由CLR用于管理对象。这些额外成员的字节数会计入对象大小。

}

  5. 然后,M2方法内部的代码开始执行。最后,M2抵达它的return语句,造成CPU的指令指针被设置成栈中的返回地址,而且M2的栈帧会展开,

  age=e.M1();

  2. 在一个最基本的方法中,会有一些"序幕"代码,负责在方法开始时做它工作之前对其进行初始化。另外,还包括了"尾声"代码,负责在方法完成工作之后对其进行清理,然后才返回至调用者。M1方法开始执行时,它的"序幕"代码就会在线程栈上分配局部变量name的内存,如图4-3所示。

internal sealed class Manager:Employee

  分分快三计划 21

  Employee e = o as Employee;
  if ( e != null ){
      //在if中使用e
  }

  is操作符通常这样使用:

   分分快三计划 22

  

  分分快三计划 23

 

Equals(Object) 确定指定的对象是否等于当前对象。如果两个对象具有相同值就返回ture.
GetHashCode 返回对象的值得一个哈希码。如果某个类型的对象要在哈希表集合中作为key使用,该类型应该重写这个方法。方法应该为不同的对象提供一个良好的分布。
ToString 该方法默认返回类型的完整名称(this.GetType().FullName)。
GetType 返回从Type派生的一个对象的实例,指出调用GetType的那个对象是什么类型。返回的Type类型可以与反射类配合使用,从而获取与对象的类型相关的元数据信息。

   

//隐式派生自System.Object
class Employee {
    .....
}

//显示派生子 System.Object
class Employee : System.Object {
  .....  
}

  CLR最重要特性之一就是类型的安全性。在运行时,CLR始终知道一个对象的类型,可以调用GetType方法,得到对象的类型。

  C#专门提供了 as 操作符,目的就是简化这种代码的写法,同时提升性能。

  as操作符的工作方式与强制类型转换一样,只是它是不会抛出异常的,如果不能转化,结果就是null。所以,正确的做法就是检查最终生成的引用是否为null。如果企图直接使用转换后的引用,就会抛出异常。

  C#不要求使用特殊语法即可将一个对象转换为它的任何及类型,因为向基类型的转换被认为是一种安全的隐式转换。但是,将对象转换为它的某个派生类时,C#要求开发人员只能进行显示转换,因为这样的转换在运行时可能失败。

  CLR允许将一个对象转换为它的实际类型或者它的任何基类型。

  3)它初始化对象的"类型对象指针"和"同步块索引"成员。

  CLR运作关系

  现在将解释类型、对象、线程栈和托管堆在运行时的相互联系。此外,还将解释调用静态方法、实例方法和虚方法的区别。

 

  我们先从线程栈开始。


  System.Object的受保护方法  

  8. M3的下一行调用Employee的非虚实例方法GetYearsEmployed。调用一个非虚实例方法时,JIT编译器会找到与"发出调用的那个变量(e)的类型(Emplyee)"对应的类型对象(Employee类型对象)。在本例中,变量e被定义成为一个Employee。如果Employee类型没有定义这个方法,JIT编译器会回溯类层次结构(一直到Object),并在沿途的每个类型中查找该方法。之所以能这样回溯,是因为每个类型对象都有一个字段引用了它的基类型,但在图中没有显示。然后,JIT编译器在类型对象的方法表中查找引用了被调用方法的记录项,对方法进行JIT编译(如果需要的话),再调用JIT编译后的调用。在本例中,假定Employee的GetYearsEmployed方法返回5,。这个整数就保存在局部变量year中。如图4-11所示。

  as操作符通常这样使用:

  

   分分快三计划 24


  现在,我们总算理解了CLR的整个类型系统及其工作方式。System.Object的GetType方法返回的是存储在指定对象的"类型对象指针"成员中的地址。也就是说,GetType方法返回的是指向对象的类型对象的一个指针。这样一来,就可以判断系统中任何对象(包括类型对象本身)的真实类型。

MemberwiseClone 这个非虚方法能创建类型的一个新实例,并将对象的实例字段设为与this对象的实例字段完全一致。返回的是对新实例的一个引用
Finalize 在垃圾回收器判断对象应该被作为垃圾收集之后,在对象的内存被实际回收之前,会调用这个虚方法。需要在回收之前执行一些清理工作的类型应该重写这个方法。

本文由分分快三计划发布,转载请注明来源

关键词: 分分快三计划 【09】CLR via CLR via C#