一、CLR的执行模型(2)
1.3 加载公共语言运行时
- 生成的每个程序集即可以是可执行应用程序,也可以是DLL(其中含有一组由可执行程序使用的类型)。当然,最终是由CLR管理这些程序集中的代码的执行。
1.4 执行程序集代码
- 托管程序集同时包含元数据和IL。IL是与CPU无关的机器语言,是Microsoft 在请教了外面几个商业及学术性语言/编译器作者之后,费尽心思开发出来的。
- IL能访问和操作对象类型,并提供了指令和初始化对象、调用对象的虚方法以及直接操作数组对象。甚至提供了抛出和捕捉异常的指令来实现错误处理。可将IL视为一种面向对象的机器语言。
- 注意,高级语言通常只公开了CLR全部功能的一个子集。然而,IL汇编语言允许开发人员访问CLR的全部功能。
- 重要提示: 在我看来,允许在不同编程语言之间方便地切换,同时又保持紧密集成,这是CLR的一个很出众的特点。遗憾的是,许多开发人员都忽视了这一点。例如,C#和VB等语言能很好地执行I/O操作部分可用C#编写,工程计算部分则换用APL编写。CLR在这些语言之间提供了其他技术无法媲美的集成度,使“混合语言编程”成为许多开发项目一个值得慎重考虑的选择。
- 执行程序集过程
图1.4.1 方法首次调用
说明:
1.在调用Main方法之前,CLR会检测出Main的代码引用的所有类型。这导致CLR分配一个内部数据结构来管理对引用类型的访问。图1.4.1 Main 方法引用了一个Console类型,导致CLR分配一个内部结构。在这个内部结构中,Console 类型的每个方法都用一个对应的记录项。每个记录项都含有一个地址,根据地址即可找到方法的实现,对这个结构初始化时CLR将每个记录项都设置成(指向)包含在CLR内部的一个未编档函数)。该函数称为JITCompiler
2.Main方法首次调用WriteLine 时,JITCompiler 函数会被调用。JITCompiler 函数负责将方法的IL代码编译成本机CPU指令,由于IL是“即时” 编译的,所以CLR这个组件称为JITter 或JIT编译器。
3. JITCompiler 函数被调用时,它知道要调用的时哪个方法,以及具体是什么类型定义了该方法。然后,JITCompiler会在定义(该类型的)程序集的元数据中查找被调用方法IL。接着JITCompiler验证IL代码,并将IL代码编译成本机CPU指令。本机CPU指令保存到动态分配的内存块中。然后,JITCompiler 回到CLR为类型创建的内部数据结构,找到与被调用方法对应的那条记录,修最初对JITCompiler 的引用,使其指向内存块(其中包含了刚才编译好的本机CPU指令)的地址。最后JITCompiler 函数跳转到内存块中的代码。这些代码正是WriteLine 方法(获取单个String参数的那个版本)的具体实现。代码执行完毕并返回时,会回到Main中的代码,并像往常一样执行。
图1.4.2方法第二次调用
说明:
1. Main 要第二次调用WriteLine 。这一次,由于已对WriteLine 的代码进行了验证和编译,所以会直接执行内存块中的代码,完全跳过JITCompiler 函数,WriteLine 方法执行完毕后,会再次回到Mian。
总结:
- JIT编译器将本机CPU 指令存储到动态内存中。
- 应用程序终止,编译好的代码也会被丢弃。
- JIT编译器必须再次将IL编译成本机指令。对于某些应用程序,这可能显著增加内存耗用。 相比之下,本机应用程序的只读代码页可由正在运行的所有实例共享
- 对于大多数应用程序,JIT编译造成的性能损失并不显著。大多数应用程序都反复调用相同 的方法。应用程序运行期间,这些方法只会对性能造成一次影响,方法内部花费的 时间 比 调用方法上的时间多得多。
托管代码与非托管代码理解:
- 非托管代码时针对一种具体CPU编译的,一旦调用,代码就能执行。 在托管环境中,代码编译分两个阶段完成,首先需要遍历源代码,做大量工作来生成IL代码。但真正想执行,这些IL代码本身必须在运行时编译成本机CPU 指令,这需要分配更多非托管内存,并要花费额外的CPU时间
- 当JIT 编译器运行时将IL编译成本机代码时,编译器对执行环境的认识比非托管编译器认识更加深刻。