一、公共语言运行库概述
公共语言运行库的功能通过公开编译器和工具,您可以编写利用此托管执行环境的代码。使用基于公共语言运行库的语言编译器开发的代码称为托管代码;托管代码具有许多优点,例如:跨语言集成、跨语言异常处理、增强的安全性、版本控制和部署支持、简化的组件交互模型、调试和分析服务等。
若要使公共语言运行库能够向托管代码提供服务,语言编译器必须生成一些元数据来描述代码中的类型、成员和引用。元数据与代码一起存储;每个可加载的公共语言运行库可移植执行 (PE) 文件都包含元数据。公共语言运行库使用元数据来完成以下任务:查找和加载类,在内存中安排实例,解析方法调用,生成本机代码,强制安全性,以及设置运行时上下文边界。
公共语言运行库自动处理对象布局并管理对象引用,当不再使用对象时释放它们。按这种方式实现生存期管理的对象称为托管数据。垃圾回收消除了内存泄漏以及其他一些常见的编程错误。如果您编写的代码是托管代码,则可以在 .NET Framework 应用程序中使用托管数据、非托管数据或者同时使用这两种数据。由于语言编译器会提供自己的类型(如基元类型),因此您可能并不总是知道(或需要知道)这些数据是否是托管的。
有了公共语言运行库,就可以很容易地设计出对象能够跨语言交互的组件和应用程序。也就是说,用不同语言编写的对象可以互相通信,并且它们的行为可以紧密集成。例如,可以定义一个类,然后使用不同的语言从原始类派生出另一个类或调用原始类的方法。还可以将一个类的实例传递到用不同的语言编写的另一个类的方法。这种跨语言集成之所以成为可能,是因为基于公共语言运行库的语言编译器和工具使用由公共语言运行库定义的通用类型系统,而且它们遵循公共语言运行库关于定义新类型以及创建、使用、保持和绑定到类型的规则。
所有托管组件都带有生成它们所基于的组件和资源的信息,这些信息构成了元数据的一部分。公共语言运行库使用这些信息确保组件或应用程序具有它需要的所有内容的指定版本,这样就使代码不太可能由于某些未满足的依赖项而发生中断。注册信息和状态数据不再保存在注册表中(因为在注册表中建立和维护这些信息很困难)。取而代之的是,有关您定义的类型(及其依赖项)的信息作为元数据与代码存储在一起,这样大大降低了组件复制和移除任务的复杂性。
语言编译器和工具公开公共语言运行库的功能的方式对于开发人员来说不仅很有用,而且很直观。这意味着,公共语言运行库的某些功能可能在一个环境中比在另一个环境中更突出。您对公共语言运行库的体验取决于所使用的语言编译器或工具。例如,如果您是一位 Visual Basic 开发人员,您可能会注意到:有了公共语言运行库,Visual Basic 语言的面向对象的功能比以前多了。下面是公共语言运行库的一些优点:
·性能得到了改进。
·能够轻松使用用其他语言开发的组件。
·类库提供的可扩展类型。
·新的语言功能,如面向对象的编程的继承、接口和重载;允许创建多线程的可缩放应用程序的显式自由线程处理支持;结构化异常处理和自定义属性支持。
如果使用 Microsoft® Visual C++® .NET,则可以使用 Visual C++ 来编写托管代码。Visual C++ 提供了托管执行环境以及对您所熟悉的强大功能和富于表现力的数据类型的访问等优点。其他公共语言运行库功能包括:
·跨语言集成,特别是跨语言继承。
·垃圾回收,它管理对象生存期,使引用计数变得不再必要。
·自我描述的对象,它使得使用接口定义语言 (IDL) 不再是必要的。
·编译一次即可在任何支持公共语言运行库的 CPU 和操作系统上运行的能力。
还可以使用 C# 语言编写托管代码。C# 语言提供了下列优点:
·完全面向对象的设计。
·非常强的类型安全。
·很好地融合了 Visual Basic 的简明性和 C++ 的强大功能。
·垃圾回收。
·类似于 C 和 C++ 的语法和关键字。
·使用委托取代函数指针,从而增强了类型安全和安全性。函数指针通过 unsafe C# 关键字和 C# 编译器 (Csc.exe) 的 /unsafe 选项可用于非托管代码和数据。
二、托管执行过程
托管执行过程包括下列步骤:
·选择编译器。
为获得公共语言运行库提供的优点,必须使用一个或多个针对运行库的语言编译器。
·将代码编译为 Microsoft 中间语言 (MSIL)。
编译将源代码翻译为 MSIL 并生成所需的元数据。
·将 MSIL 编译为本机代码。
在执行时,实时 (JIT) 编译器将 MSIL 翻译为本机代码。在此编译过程中,代码必须通过验证过程,该过程检查 MSIL 和元数据以查看是否可以将代码确定为类型安全。
·运行代码。
公共语言运行库提供使执行能够发生以及可在执行期间使用的各种服务的结构。
三、自动内存管理
自动内存管理是公共语言运行库在托管执行过程过程中提供的服务之一。公共语言运行库的垃圾回收器为应用程序管理内存的分配和释放。对开发人员而言,这就意味着在开发托管应用程序时不必编写执行内存管理任务的代码。自动内存管理可解决常见问题,例如,忘记释放对象并导致内存泄漏,或尝试访问已释放对象的内存。
1、分配内存
初始化新进程时,运行时会为进程保留一个连续的地址空间区域。这个保留的地址空间被称为托管堆。托管堆维护着一个指针,用它指向将在堆中分配的下一个对象的地址。最初,该指针设置为指向托管堆的基址。托管堆上部署了所有引用类型。应用程序创建第一个引用类型时,将为托管堆的基址中的类型分配内存。应用程序创建下一个对象时,垃圾回收器在紧接第一个对象后面的地址空间内为它分配内存。只要地址空间可用,垃圾回收器就会继续以这种方式为新对象分配空间。
从托管堆中分配内存要比非托管内存分配速度快。由于运行时通过为指针添加值来为对象分配内存,所以这几乎和从堆栈中分配内存一样快。另外,由于连续分配的新对象在托管堆中是连续存储,所以应用程序可以快速访问这些对象。
2、释放内存
垃圾回收器的优化引擎根据所执行的分配决定执行回收的最佳时间。垃圾回收器在执行回收时,会释放应用程序不再使用的对象的内存。它通过检查应用程序的根来确定不再使用的对象。每个应用程序都有一组根。每个根或者引用托管堆中的对象,或者设置为空。应用程序的根包含全局对象指针、静态对象指针、线程堆栈中的局部变量和引用对象参数以及 CPU 寄存器。垃圾回收器可以访问由实时 (JIT) 编译器和运行时维护的活动根的列表。垃圾回收器对照此列表检查应用程序的根,并在此过程中创建一个图表,在其中包含所有可从这些根中访问的对象。
不在该图表中的对象将无法从应用程序的根中访问。垃圾回收器会考虑无法访问的对象垃圾,并释放为它们分配的内存。在回收中,垃圾回收器检查托管堆,查找无法访问对象所占据的地址空间块。发现无法访问的对象时,它就使用内存复制功能来压缩内存中可以访问的对象,释放分配给不可访问对象的地址空间块。在压缩了可访问对象的内存后,垃圾回收器就会做出必要的指针更正,以便应用程序的根指向新地址中的对象。它还将托管堆指针定位至最后一个可访问对象之后。请注意,只有在回收发现大量的无法访问的对象时,才会压缩内存。如果托管堆中的所有对象均未被回收,则不需要压缩内存。
为了改进性能,运行时为单独堆中的大型对象分配内存。垃圾回收器会自动释放大型对象的内存。但是,为了避免移动内存中的大型对象,不会压缩此内存。
3、级别和性能
为了优化垃圾回收器的性能,托管堆分为三个生成级别:0、1 和 2。运行时的垃圾回收算法基于以下几个普遍原理,这些垃圾回收方案的原理已在计算机软件业通过实验得到了证实。首先,压缩托管堆的一部分内存要比压缩整个托管堆速度快。其次,较新的对象生存期较短,而较旧的对象生存期则较长。最后,较新的对象趋向于相互关联,并且大致同时由应用程序访问。
运行时的垃圾回收器将新对象存储在第 0 级托管堆中。在应用程序生存期的早期创建的对象如果未被回收,则被升级并存储在第 1 级和第 2 级托管堆中。对象升级的过程将在本主题的后面介绍。因为压缩托管堆的一部分要比压缩整个托管堆速度快,所以此方案允许垃圾回收器在每次执行回收时释放特定级别的内存,而不是整个托管堆的内存。
实际上,垃圾回收器在第 0 级托管堆已满时执行回收。如果应用程序在第 0 级托管堆已满时尝试新建对象,垃圾回收器将会发现第 0 级托管堆中没有可分配给该对象的剩余地址空间。垃圾回收器执行回收,尝试为对象释放第 0 级托管堆中的地址空间。垃圾回收器从检查第 0 级托管堆中的对象(而不是托管堆中的所有对象)开始执行回收。这是最有效的途径,因为新对象的生存期往往较短,并且期望在执行回收时,应用程序不再使用第 0 级托管堆中的许多对象。另外,单独回收第 0 级托管堆通常可以回收足够的内存,这样,应用程序便可以继续创建新对象。
垃圾回收器执行第 0 级托管堆的回收后,会压缩可访问对象的内存,如本主题前面的释放内存中所述。然后,垃圾回收器升级这些对象,并考虑第 1 级托管堆的这一部分。因为未被回收的对象往往具有较长的生存期,所以将它们升级至更高的级别很有意义。因此,垃圾回收器在每次执行第 0 级托管堆的回收时,不必重新检查第 1 级和第 2 级托管堆中的对象。
在执行第 0 级托管堆的首次回收并把可访问的对象升级至第 1 级托管堆后,垃圾回收器将考虑第 0 级托管堆的其余部分。它将继续为第 0 级托管堆中的新对象分配内存,直至第 0 级托管堆已满并需执行另一回收为止。这时,垃圾回收器的优化引擎会决定是否需要检查较旧的级别中的对象。例如,如果第 0 级托管堆的回收没有回收足够的内存,不能使应用程序成功完成创建新对象的尝试,垃圾回收器就会先执行第 1 级托管堆的回收,然后再执行第 0 级托管堆的回收。如果这样仍不能回收足够的内存,垃圾回收器将执行第 2、1 和 0 级托管堆的回收。每次回收后,垃圾回收器都会压缩第 0 级托管堆中的可访问对象并将它们升级至第 1 级托管堆。第 1 级托管堆中未被回收的对象将会升级至第 2 级托管堆。由于垃圾回收器只支持三个级别,因此第 2 级托管堆中未被回收的对象会继续保留在第 2 级托管堆中,直到在将来的回收中确定它们为无法访问为止。
4、为非托管资源释放内存
对于应用程序创建的大多数对象,可以依赖垃圾回收器自动执行必要的内存管理任务。但是,非托管资源需要显式清除。最常用的非托管资源类型是包装操作系统资源的对象,例如,文件句柄、窗口句柄或网络连接。虽然垃圾回收器可以跟踪封装非托管资源的托管对象的生存期,但却无法具体了解如何清理资源。创建封装非托管资源的对象时,建议在公共 Dispose 方法中提供必要的代码以清理非托管资源。通过提供 Dispose 方法,对象的用户可以在使用完对象后显式释放其内存。使用封装非托管资源的对象时,应该了解 Dispose 并在必要时调用它。