公共语言运行时
.NET Framework的核心组件是公共语言运行时(Command Language Runtime,CLR),主要功能包括内存管理、线程管理、代码执行、代码安全验证、编译、系统服务和远程处理等核心服务。这些功能是公共语言运行时在运行托管代码时的固有模块,如图1-2所示。
图1-2  公共语言运行时
1.公共类型系统
编程语言都有类似的特点,.NET平台利用不同语言这个相近的共性,抽象出了公共类型系统(CTS)。CTS是多信息类型系统,被内置在公共语言运行时中,定义了声明和使用类型的标准,使得CLR可以在不同语言开发的应用程序之间管理这些标准化的类型,并且在不同计算机之间以标准化的格式进行数据通信。它具有以下功能:
CTS定义了所有应用程序使用的主要.NET数据类型,以及这些类型的内部格式。例如,CTS定义了整型是32位大小,还指定了整型值的内部格式。
CTS指定了如何为结构和类分配内存。
CTS允许不同语言开发的组件可以互操作。
CTS实施类型安全性,它禁止一个应用程序使用为另一个应用程序分配的内存。
由于公共类型系统包含创建新数据类型的规则,所以开发人员并不受限于少量的数据类型。特别是,开发人员可以定义他们自己的值或他们自己的类,而且只要这些值或类符合公共类型系统的规则,它们将是公共类型系统可以接受的。只要开发人员正在使用.NET语言,定义CTS可接受数据类型的操作将是透明的。遵守CTS规则的事宜将由编译程序负责。公共类型
系统管理了许多类别的数据类型,如图1-3所示:
图1-3  公共类型系统的基本结构
CTS的每一种类型都是对象,并继承自一个基类——System.Object。它不仅定义了所有的数据类型,而且提供了面向对象的模型以及各种语言需要遵守的标准。CTS可以分为两个大类:值类型和引用类型,同时这两种类型之间还可以进行强制转换,这种转换被称为装箱(Boxing)和拆箱(UnBoxing)。
2.公共语言规范
要和其他对象完全交互,而不管这些对象是以何种语言实现的,对象必须只向调用方公开那些它们必须与之互用的所有语言的通用功能。为此定义了公共语言规范(CLS),它是许多应用程序所需的一套基本语言功能。
公共语言规范(CLS)可为库编写者和编译器编写者提供指南,使任何支持CLS的语言都完全使用库,并使用这些语言相互集成。公共语言规范是公共类型系统的子集,这两个集合一起定义了所有.NET编程语言的标准集,允许这些编程语言编写的应用程序可以相互通信和操作。
CLS和.NET自身都依赖于Windows API提供的底层服务,例如菜单、按钮、列表框和标签等基本的Windows窗体控件类,以及基本的Windows服务来管理文件、进程和内存。
CLS定义了所有基于.NET Framework的语言都必须支持的最小功能集。其规则可以概括如下:
定义了命名变量的标准规则。例如,与CLS兼容的变量名都必须以字母开始,并且不能包含空格。变量名之间必须有所区别,除了变量名之间的大小写之外。
定义了原语数据类型,如Int32,Int64,Single,Double和Boolean。
禁止无符号数值数据类型。有符号数值数据类型的一个数据位被保留来指示数值的正负。无符号数据类型没有保留这个数据位。
定义了对支持基于0的数组的支持。
指定了函数参数列表的规则,以及参数传递给函数的方式。例如,CLS禁止使用可选的参数。
定义了事件名和参数传递给事件的规则。
禁止内存指针和函数指针。但是可以通过委托提供类型安全的指针。
除了上述标准之外,CLS还定义了其他标准。任何语言都可以扩展基本的CLS需求。例如,有些语言支持无符号整型。不鼓励使用非标准的功能,因为这样做就妨碍了语言之间的互操作性。完全符合CLS的语言称为兼容的CLS的语言。
3.中间语言
在.Net框架中,公共语言基础结构使用公共语言规范来绑定不同的语言。通过要求不同的语言至少要实现公共类型系统(CTS)包含在公共语言规范中的部分,公共语言基础结构允许不同的语言使用.Net框架。因此在.Net框架中,所有的语言(C#,VB.Net,Effil,NET等)最后都被转换为了一种通用语言:微软中间语言(MSIL)。
MSIL是将.Net代码转化为机器语言的一个中间过程。它是一种介于高级语言和基于Intel的汇编语言的伪汇编语言。当用户编译一个.Net程序时,编译器将源代码翻译成一组可以有效地转换为本机代码且独立于CPU的指令。当执行这些指令时,实时(JIT)编译器将它们转化为CPU特定的代码。
由于公共语言运行库支持多种实时编译器,因此同一段MSIL代码可以被不同的编译器实时编译并运行在不同的结构上。从理论上来说,MSIL将消除多年以来业界中不同语言之间的纷争。在.Net的世界中可能出现下面的情况:一部分代码可以用Effil实现,另一部分代码使用C#或VB完成的,但是最后这些代码都将被转换为中间语言。这给程序员提供了极大的灵活性,程序员可以选择自己熟悉的语言,并且再也不用为学习不断推出的新语言而烦恼了。中间语言的主要特征如下:
面向对象和使用接口
值类型和引用类型之间的巨大差别
编程语言有哪些类型强数据类型
使用异常来处理错误
使用特性(attribute)
中间语言的格式类似于程序集语言。程序集语言的语句直接与内部CPU体系结构支持的指令相关。但是中间语言的格式通常不依赖于特定CPU的体系结构。也就是说,中间语言不直接引用CPU寄存器或者执行CPU指令。当用户执行中间语言格式的应用程序时,另一个名为Just-In-Time(JIT)编译器的实用程序进一步把中间语言转换为目标CPU可以执行的本机可执行文件。使用JIT编译器把中间语言文件转换为本机可执行文件的过程称为JITting。
4.托管执行过程
在将代码编译为MSIL再用JIT编译器将其编译为本机代码后,CLR的任务并没有全部完成。
用.NET Framework编写的代码在执行时往往处于被托管的状态,即CLR管理着应用程序。
在实现代码托管时,代码必须向运行时提供最小级别的信息(元数据)。在默认状态下,所有C#、VisualBasic.NET和JScript.NET代码都是托管代码。C++.NET代码在默认情况下不是托管代码,但通过指定的命令行开关,编译器也可以生成托管代码。
托管代码的意义在于可以防止多个正在执行的应用程序相互干扰,一个应用程序不会覆盖另一个应用程序分配的内存。这个过程被称为类型安全性(Type Safety)。
与托管代码密切相关的是托管数据,托管数据是CLR的垃圾回收期进行分配和释放的数据。与托管代码类似,在默认情况下C#、VisualBasic.NET和JScript.NET数据都是托管数据,C++.NET在默认情况下为非托管数据。不过通过关键字,C#数据也可以被标记为非托管数据。创建托管代码的步骤如下。
首先选择一个合适的编译器,使之可以生成适合CLR执行的代码,并且使用.NET Framework提供的资源。微软公司目前提供4种兼容.NET Framework的语言,并允许其他第三方公司提供.NET Framework语言。
然后,再把应用程序编译为独立于机器的中间语言。在执行时,必须对中间语言代码进行JIT编译,将其转换为本机可执行的程序。最后,在应用程序执行时,调用.NET Framework和CLR提供的资源。以上就是托管执行过程,如图1-4所示。
图1-4  创建托管代码的流程
托管代码具有许多优点,如下所示。
语言的互操作性
使用IL不仅支持平台无关性,还支持语言的互操作性,就是能将任何一种语言编译为中间代码,编译好的代码可以与其他语言编译过来的代码交互操作。
高性能
在实际应用中,IL比Java字节码作用更大,IL总是即时编译的,而Java字节码则仅仅是解释性的。JIT并不是把整个程序依次编译完(这样将会使程序启动时间十分漫长)而只是编译程序调用的部分代码。某些代码编译过依次后,就不需要再重新编译了。
微软公司认为这个过程要比一开始就编译整个应用程序代码效率高一些,因为任何应用程序的大部分代码实际上并不是在每次运行时都执行。这也解释了为何托管IL代码的执行速度几乎与内部机器代码的执行速度差不多。
平台无关性
托管代码可以通过.NET Framework即时编译中间语言,因此开发者编写的代码与具体的操作平台没有直接关联,只要平台安装了.NET Framework,则编写的代码就可以在该平台上执行。这一特点与Java语言十分类似。
5.自动内存管理
在使用传统的编程语言(例如C、C++等)开发程序时,开发者往往需要自行编写代码来管理内存,为数据分配内存空间并不断地释放一些无用的空间。
自动内存管理是CLR在托管执行过程中提供的服务之一。它可以由.NET Framework来管理内存的分配和释放,对于开发者而言,这就意味着开发托管应用程序时省略了管理内存的步骤,避免了忘记释放对象导致的内存泄漏。
释放内存
在托管模式下,每个应用程序都有一组根,这些根包含全局对象指针、静态对象指针、线程堆栈中的局部变量和引用对象参数以及CPU寄存器。每个根或引用托管堆中的对象,或设置为空。
GC可以访问由实时JIT编译器和运行时维护的活动根的列表,对照此列表检查应用程序的根,并在此过程中创建一个图表,包含所有可从这些根中访问的对象。另外,GC的优化引擎会根据所执行的分配决定执行回收的最佳时间,还会在执行回收时通过检查应用程序的根来确定程序不再使用的对象,并释放该对象占据的内存。
在回收的过程中,GC检查托管堆,查无法访问对象所占据的地址空间块,发现无法访问的对象时,就是用内存复制功能来压缩内存中可访问对象,释放分配给不可访问对象的地址空间块。在压缩了可访问对象的内存后,GC就会作出必要的指针更正,一边应用程序的根指向新地址中的对象。
只有在GC发现大量无法访问对象时,才会启用压缩内存机制。如果托管堆中的所有对象均未被回收,则不需要压缩内存。
为了改进性能,CLR为单独堆中的大型对象分配内存,GC会自动释放大型对象的内存。但是,为了避免移动内存中的大型对象,不会压缩此内存。
为非托管资源释放内存
除了为托管的资源释放内存外,CLR还可以为非托管资源释放内存。但是,这一过程需要显式清除。最常用的非托管资源类型是封装操作系统资源的对象,例如文件句柄、窗口句柄或网络连接等。
虽然GC可以跟踪封装非托管资源托管对象的生存期,但却无法具体了解如何清理资源。创建封装非托管资源的对象时,建议在公共Dispose方法中提供必要的代码以清理非托管资源。通过提供Dispose方法,对象的用户可以在使用完对象后显式释放其内存。