CLR 基础-运行时的相互关系

CLR 基础-运行时的相互关系
 
解释类型、对象、线程栈和托管堆在运行时的相互关系。本文内容都是来自《CLR via C#》,这里只是做整理和梳理知识。
 
1 所有类型都从System.Object派生
 
所有类型最终都从System.Object 派生,所以每个类型的每个对象都保证了一组最基本的方法。
 
system.object的公共方法
 
    1 Equals
    2 GetHashCode
    3 ToString
    4 GetType
 
system.object的受保护方法
    
    MemberwiseClone:这个非虚方法创建类型的新实例,并将新对象的实例字段与this 对象的实例字段完全一致。返回对新实例的引用。
    Finalize:在垃圾回收器判断对象应该作为垃圾被回收之后,在对象的内存被实际回收之前,会调用这个虚方法。
 
 
CLR要求所有对象都用new 操作符来创建,以下是new 操作符所做的事情。
  1.  计算类型及其所有基类型(一直到System.Object)中定义的所有实例字段需要的字节数。堆上每对象都需要一些额外的成员,包括“类型对象指针 Type object pointer”和“同步块索引 sync block index”。CLR利用这些成员管理对象。
  2. 从托管堆中分配类型要求的字节数,从而分配对象的内存,分配的所有字节都设为零。
  3. 初始化对象的“类型对象指针”和“同步块索引”成员。
  4. 调用类型的实例构造器,传递在new调用中指定的参数。
 
 
2 类型转换
 
CLR最终要的特性之一就是类型安全。运行时,CLR总是知道对象是什么类型。因为GetType方法不是虚方法,一个类型不可能重写GetType返回一个其他类型。
 
  • 一个对象转换为它的基类是可以隐式的转换,
  • 然而将对象转换为它的某个派生类时,C#要求开发人员只能显示转换。
 
 
3 is 和 as 操作符来转型
 
is 操作符:
  • is 检查对象是否兼容于指定类型,返回 Boolean 值 true 或 false。
  • 注意,is 操作符永远不跑出异常。
  • 如果对象引用 null,is 操作符总是返回 false,因为没有可检查其类型的对象。
 
 
as 操作符:
如下代码所示:CLR 核实 o 是否兼容于 Employee 类型。
  • 如果是,as 返回非null 引用。
  • 如果 o不兼容于 Employee 类型,as 返回 null。
  • 注意:as 操作符只校验一次对象类型,if 只检查 e 是否为null。
 
 
4 运行时的相互关系
 
解释类型、对象、线程栈、和托管堆在运行时的相互关系。此外,还将解释调用静态方法,实例方法和虚方法的区别。
 
如下图展示了已加载 CLR 的一个Windows 进程。该进程可能有多个线程。线程在创建时会得到1MB 的栈,方法的参数和方法内部的局部变量都在这个栈上。现在,假定线程执行的代码要掉调用 M1 方法。
 
 
最简单的方法包含“序幕”代码,在方法开始工作前对其进行初始化;还包含“尾声”代码,在方法做完工作的时候进行清理。M1方法的序幕代码,在线程栈上分配局部变量 name 的内存。
 
 
 
 
然后,M1 调用 M2 方法,将局部变量 name 作为实参传递。M2 方法内部使用参数变量 s 标识栈位置。调用方法时还会将“返回地址”压入栈。
 
 
M2 方法的序幕代码在线程栈中为局部变量 length 和 tally 分配内存。最终,M2 抵达它的return 语句,造成 CPU 的指令指针被设置成栈中的返回地址。M2 的栈帧unwind,恢复成tu 4-3 的样子。之后,M1 继续执行M2 调用之后的代码,M1 的栈帧将准确反映 M1 的状态。
 
 
 
现在,让我们围绕CLR 来调整一下讨论。假定有以下两个类型定义:
 
Windows 进程已经启动,CLR已加载到其中,托管堆已初始化,而且已创建一个线程。线程已执行了一些代码,马上就要调用M3 方法。如图4-6 展示目前状态。M3 方法包含的代码将演示 CLR 如何工作.
 
JIT编译器将M3 的IL代码转换成本机CPU 指令时,会注意到M3 内部引用的所有类型。这时CLR 要确认定义了这些类型的所有程序集都已经加载。然后利用这些元数据,CLR提取与这些类型有关的信息,创建一些数据结构表示类型本身。
图4-7 展示了为 Employee 和 Manager 类型对象使用的数据结构。 定义类型时,可在类型内部定义静态数据字段。为这些静态数据字段提供支援的字节在类型对象自身中分配。
当CLR 确认方法需要的所有类型对象都已创建,M3 的代码已经编译之后,就允许线程执行M3 的本机代码。M3 的序幕代码执行时必须在线程栈中为局部变量分配内存。
 
 
 
然后,M3 执行代码构造一个 Manager 对象。这造成在托管堆上创建 Manager 类型的一个实例。该对象还包含必要的字节来容纳 Manager 类型定义的所有实例数据字段,以及容纳由 Manager 的任何基类(本例就是 Employee 和 object)定义的所有实列字段。
new 操作符返回 Manager 对象的内存地址,该地址保存到变量e 中(e 在线程栈上)。
 
M3 的下一行代码调用Employee 的静态方法Lookup。假设Lookup方法会查询数据库,并返回一个Manager 对象将它赋值给变量 e。注意,e 不再引用第一个Manager 对象,该对象没有变量引用它,所以是下次垃圾回收的主要目标。
 
 
M3 的下一行代码调用 Employee 的非虚实列方法 GetYearsEmployed。调用非虚实例方法时,JIT 编译器会找到 “发出调用的那个变量(e)的类型” 对应的类型对象,也就是 Employee 类型对象。JIT 编译器会检查 Employee 类型有没有要调用的方法,如果没有再回溯类层次结构(也就是向基类查找有没有该方法)。
 
JIT 编译器在 Employee类型的方法表中,找到该方法的记录项了,对方法进行JIT 编译,再调用JIT 编译好的代码。
 
假设 GetYearsEmployed 方法返回5 ,这个数据被保留再变量 year 中。这个操作如图11 所示。
 
 
M3 的下一行调用 Employee 的虚实例方法 GetProgressReport。
 
  1. 调用虚实例方法时,JIT 编译器要在方法中生成一些额外的代码:方法每次调用都会执行这些代码。
  2. 这些代码首先检查发出调用的变量,并跟随地址来到发出调用的对象。就是从变量e 找到 Manager对象。
  3. 然后代码检查 Manager 对象内部的“类型对象指针”成员,该成员指向对象的实际类型。
  4. 然后,代码在 Manager 类型对象的方法表中查找引用了被调用方法的记录项,对方法进行 JIT 编译,再调用 JIT 编译好的代码。
由于目前e 引用一个 Manager对象,所以会调用 Manager 的 GetProgressReport 实现。
 
注意: 如果变量e 引用的是 Employee 对象,它的类型对象指针将引用 Employee 类型对象。这样最终执行的就是 Employee 的 GetProgressReport 实现,而不是 Manager的。
 
至此,我们讨论了源代码、IL 和 JIT 编译的代码之间的关系。还讨论了线程栈、实参、局部变量以及这些实参和变量如何引用托管堆上的对象。讨论了 JIT 编译器如何决定静态方法、非虚实例方法和 虚实列方法的调用方式。理解这些之后,可以深入地认识 CLR 的工作方式。
 
 
 
注意 ,Employee 和 Manager 类型对象也包含 “类型对象指针”成员。因为类型对象本质也是对象。CLR 创建类型对象时,也要初始化这些成员。但是初始化成什么呢?
 
CLR 开始在一个进程中运行时,会立即为 MSCorLib.dll 中定义的 System.Type 类型创建一个特殊的类型对象。Employee 和 Manager 类型对象都是该类型的 “实例”。因此,它们的类型对象指针成员会初始化成对 System.type 类型对象的引用,如图13 所示。
 
那System.Type 类型对象本身也是对象,内部也有“类型对象指针”成员,这个指针指向什么?答案是:指向它本身(这和 Python的类型系统相似),因为System.Type 类型对象本身是一个类型对象的“实例”。
 
 
现在我们总算理解了 CLR 的整个类型系统及其工作方式。(*^_^*)

posted on 2019-09-04 22:20  拾掇的往昔  阅读(115)  评论(0编辑  收藏  举报

导航