公共语言运行时 (CLR) 和 Java 运行时环境 (JRE)






4.95/5 (57投票s)
2002年2月13日
30分钟阅读

370438
本文讨论解释器、编译器、JVM 和 CLR
引言
互联网革命重新点燃了对平台独立性的需求。C/C++ 不适合这项任务,这时 Sun 的 Java 出现了。Java 于 1995 年由 Sun 正式推出。Java 作为一种平台独立、真正的面向对象语言被引入。本文的目的是解释 Java 如何实现其平台独立性。但这在对编译语言和解释语言有很好的理解之前是无法完全理解的;因此,本文还对编译器和解释器进行了一些阐述。本文还继续解释了 JVM(Java 虚拟机)和 CLR(公共语言运行时,它是 Microsoft .NET 技术的运行时环境)。任何关于 JVM 和 CLR 的讨论,如果不讨论即时 (JIT) 编译(本文也讨论了这一概念),都是没有意义的。
本文无意对 JVM、CLR、JIT、编译器或编程语言进行详尽的研究。它旨在提供一个关于这些零散部分如何组合在一起以实现平台独立性的宏观视图。感兴趣的读者应查阅文末提供的参考文献,以了解更多关于本文讨论的主题。
目录
计算机如何工作?
计算机硬件就像其他任何机器一样。你可以打开它,电子就会开始流过它。这就是计算机能做的全部事情。就像一个无知的生物一样——计算机需要被明确告知它应该做什么。计算机程序是告诉计算机你希望它做什么的工具。
计算机只懂一种语言——机器码。机器码是二进制(1 和 0)数字的序列。微处理器制造商(微处理器是计算机的心脏)决定哪个位序列代表什么。
想象一下你想构建自己的微处理器。你会在其中加入各种任务。并且你需要为每个任务设置一个独特的代码。计算机程序将发出这些代码以启动微处理器所需的任务。
让我们考虑将一个值移入寄存器(寄存器可以被认为是微处理器极其快速的内部存储器)的最基本任务。此任务要求微处理器从特定的内存地址读取值并将其放入特定的寄存器中。因此,您的微处理器需要知道以下内容
- 特定操作码
- 从何处读取值的内存地址
- 存放值的寄存器号。
请记住,微处理器被设计成只能对寄存器中的内容进行操作,而不能直接对内存进行操作。正是由于这个原因,我们必须在对数字进行各种操作(例如加法、减法、除法等)之前,将它们从内存移动到寄存器中。因此,`move` 操作是微处理器中最常用和最基本的操作之一。
让我们为 `move` 操作分配一个合适的操作码——“0001”。您看到此代码有什么问题吗?我们选择的代码没有问题,只是它只有 4 位宽。这只会允许我们的微处理器拥有 24 = 16 个操作。显然,我们需要远不止 16 个操作才能使我们的微处理器具有商业可行性。因此,让我们更改 `move` 操作的代码,并为其分配一个更大的值——“0000 0000 0001”。现在我们的处理器可以处理 212 = 4096 个操作。
确定操作码(op code)后,我们需要确定内存地址。
如果计算机有 512 字节的 RAM(以今天的标准来看太小了,但足以用于说明目的),并且一个 RAM 位置是 2 字节(16 位)宽,那么我们有 512 字节 / 2 字节 = 256 字节的可寻址内存位置,并且需要 28 = 256,至少 8 位来表示一个内存地址。
假设我们的微处理器有 16 个内部寄存器,因此我们需要 24 = 16,4 位来指定一个寄存器。
有了上述三项设计决策,如果一个程序希望将一个数字从 RAM 移动到寄存器中,那么它就必须向我们的微处理器发出以下机器码
0000 0000 0001 0000 0011 0010
前 12 位表示操作码,接下来的 8 位表示内存地址,再接下来的 4 位表示寄存器号。上述机器码将把内存位置“3”中保存的数字移动到寄存器“2”中。
请记住,机器语言是微处理器(因此也是计算机)唯一能理解的语言。所以所有的应用程序/软件最终都必须翻译成机器语言才能在计算机上运行。
虽然我们的微处理器很简单,但今天的商用微处理器都基于相同的原理构建。每个微处理器都有自己的操作码(如我们简单微处理器的 `move` 操作码)和自己的寻址方案。苹果电脑围绕摩托罗拉的微处理器构建,而 IBM 和 IBM 兼容电脑则都围绕英特尔处理器构建。
我们在此章节构建的微处理器是可能的最简单版本。访问[1]了解最新的微处理器开发。
什么是编译器?
让我们使用我们简单的微处理器(在计算机如何工作部分构建的)来尝试最简单的任务,即添加两个数字。此任务需要以下步骤
将第一个数字从内存移到寄存器 #1
将第二个数字从内存移到寄存器 #2
将寄存器 #1 的内容与寄存器 #2 的内容相加,并将结果放入寄存器 #3
将寄存器 #3 的内容移至内存。
我们在上面一节中为 `move` 操作制作了一个示例机器语言代码。我们的 `move` 操作可以将一个数字从内存移动到寄存器。我们还需要另一个 `move` 操作来做相反的事情——即将寄存器中的数字移动到内存位置。我们将为此类操作使用以下机器码
0000 0000 0010 0001 0000 0011
前 12 位是操作码,接下来的 4 位是寄存器号,剩余的 8 位是内存位置。请注意,此 `move` 操作码是 0000 0000 0010(等于“2”)。
接下来我们需要为 `add` 操作设计一个代码。
0000 0000 0011 0001 0010 0011
有了上述机器码,我们可以指示我们的计算机执行如下所示的加法任务
指令 1
0000 0000 0001 0000 0011 0001(将数字从内存位置“3”移动到寄存器 #1)
指令 2
0000 0000 0001 0000 0100 0010(将数字从内存位置“4”移动到寄存器 #2)
指令 3
0000 0000 0011 0001 0010 0011(将寄存器 #1 和寄存器 #2 相加,并将结果放入寄存器 #3)
指令 4
0000 0000 0010 0011 0000 0100 (将寄存器 #3 的内容移动到内存位置“4”)
上述机器码将指示计算机添加两个数字。
CPU 将执行第一条语句,然后递增其*程序计数器寄存器*。该寄存器保存下一条要执行指令的内存地址,以便现在该寄存器指向下一条指令(即指令 2)。
现在 CPU 将从 RAM 中获取指令到其缓存/寄存器中并执行它。一旦执行完毕,相同的过程将重复进行下一条指令——直到整个程序(即所有机器语言指令都已执行完毕);此时控制权将返回给操作系统。这种操作序列称为*取-执行周期*,是冯诺依曼体系结构(当今所有 PC 都围绕其构建的体系结构)的特征。应该注意的是,指令的执行时间远少于获取过程。这是因为执行是通过硬件实现的,而获取涉及数据在内存和缓存/寄存器之间来回移动。因此,在编译代码中,瓶颈是获取操作。
每个微处理器都有自己的机器码。我们极其简单的微处理器有自己的机器码,英特尔有自己的代码,摩托罗拉也有自己的代码。
对于人类来说,记住机器码并使用机器码开发哪怕是小型应用程序也几乎是不可能的。这就是高级语言的用武之地。
在 C/C++ 中,要添加两个数字,您会编写以下代码
int i;
int j;
int k;
k = i + j;
将 C 代码与机器码进行比较。用 C/C++ 等高级语言编写应用程序远比用机器语言编写相同的应用程序可行。但问题是计算机除了机器语言什么都不懂——我们需要某种翻译器,它能将高级 C/C++ 代码翻译成机器码。这种“翻译器”被称为*编译器*。
编译器是一个程序,它将 C/C++ 文件作为输入,并输出一个可执行文件,该文件可以直接在主机上运行。
正如我已经提到的存在多个(且不兼容的)微处理器,这意味着我们将为不同的硬件配备独立的编译器。因此,相同的 C 代码必须使用适用于 Apple Macintosh 的 C 编译器进行编译,才能在 Apple 计算机上运行。如果您希望相同的 C 代码在 Intel 平台上运行的 Microsoft Windows 上运行,那么您将不得不使用适用于 Windows 的 C 编译器编译您的 C 代码。
简而言之,编译器将源代码文件(一个简单的文本文件)转换为可在主机上运行的可执行文件。熟悉 C/C++ 的人会意识到这过于简化了。
您的 C/C++ 代码不会直接转换为 .exe 文件;而是转换为一个名为对象文件(.obj)的中间文件。如果您的项目中有五个 C/C++ 文件,那么编译器将生成五个 obj 文件,每个 C/C++ 文件一个;但只有一个 .exe 文件。对象文件的级别略高于原始 .exe 文件。在对象文件中,内存引用是本地的,并且 obj 文件不会链接到您的 C/C++ 程序使用的其他 obj、dll、lib 文件。
当您在 C/C++ 中使用 include 语句 `#include <myfile.h>` 时,编译器会检查 `myfile.h` 文件的存在。如果找不到,您会收到错误消息,并且编译失败。假设 `myfile.h` 文件存在,并且您使用了在 `myfile.h` 中声明的函数 `addNumber(int, int)`。编译器将检查该函数是否已在 `myfile.h` 中声明。如果该函数不存在,则编译将失败并显示错误消息。假设该函数已在 `myfile.h` 中声明。现在编译器将成功完成编译——除非存在其他错误。
编译成功后,编译器将生成一个 obj 文件,并启动链接器。链接器是一个程序,它接受项目中所有的 obj 文件,并查找所有交叉引用的文件和所有必需的库。在上面的例子中,编译器确保 `myfile.h` 存在。链接器确保 `myfile.h` 的 .lib 文件也必须存在。lib 文件是包含 `myfile.h` 中声明的所有函数的代码的文件。链接器做的另一个重要任务是将操作系统 API(应用程序编程接口)调用转换为适当的内存地址。许多操作系统提供 I/O API。因此,程序员无需重新发明轮子,而是可以在我们的程序中简单地调用这些操作系统的 API 函数。链接器知道这些函数代码所在的内存地址,并将函数调用转换为操作系统内存空间中适当的内存地址。

我将不讨论词法、语法、语义分析器和代码生成器。感兴趣的读者应参阅参考文献部分以获取这些主题的详细信息。
什么是解释器?
如果说编译器是运行编程语言的一个极端,那么纯解释器就是另一个极端。纯解释器不像编译器那样进行任何代码转换。这些解释器接收源代码(用高级语言编写),然后逐条在主机上执行语句。这些纯解释器完全无法进行任何代码优化。纯解释器也无法进行语法检查;而编译器可以。纯解释器的例子是所有操作系统都附带的脚本语言。Unix/Linux 中的 shell 脚本、Microsoft Windows 中的批处理文件(.bat)和命令文件(.cmd)都是纯解释语言的例子。当你制作一个批处理文件时,你只需编写高级代码,然后将文件保存为 .bat 扩展名。要运行你的 .bat 文件,你只需在命令行提示符下键入文件名。操作系统读取文件的第一行,并(尝试)执行第一条语句。如果执行成功,你会得到预期的结果;如果由于语法错误而无法执行,你会在命令行提示符窗口中看到“Bad command or file name”错误消息。同样适用于 Unix/Linux 中编写的 shell 脚本。
一些商业编程语言已知是解释型的,例如 BASIC、Java、Tcl/TK。然而这些语言的行为并不完全像上面描述的那样。原因很简单——没有一种流行的现代编程语言是纯解释型的。它们要么是编译型的(如 C/C++),要么采用混合方法(如 Java、BASIC、Tcl/Tk)。纯解释方法和混合方法可以用以下图表描述
纯解释器

混合编译器-解释器

从上图可以看出,当今流行的解释型语言并非纯解释型。它们遵循“编译”技术来生成中间代码(例如 Microsoft 的中间语言 - MSIL、Sun 的 Java 字节码等)。解释器处理的是这种中间语言,而不是原始的高级源代码。这种方法消除了纯解释型语言固有的许多问题,并提供了完全编译型语言的许多优点。
读者应该注意,解释器和编译器最终都会将源代码转换为机器语言;毕竟计算机只能用机器语言运行程序。编译器离线一次性完成这种转换(如什么是编译器一节所述);而解释器逐条程序语句进行这种转换。编译程序以*取-执行周期*运行,而解释程序以*解码-取-执行周期*运行。解码由解释器完成,而取和执行操作由 CPU 完成。在解释器中,瓶颈是解码阶段,因此解释程序的运行速度可能比编译程序慢 30-100%。
以下流程图比较了编译程序和解释程序的执行过程。

解释型应用程序的执行情况如下所示

从上面的流程图可以明显看出,解释型程序存在逐条解码每个语句的开销;因此在解释型程序中,瓶颈是解码过程。
读者会问自己一个显而易见的问题:“为什么有些语言被开发成解释型,而另一些则被开发成编译型?这两种方法各有什么优缺点?” 这就是下一节的主题。
编译语言和解释语言的优缺点
语言可以开发成完全编译型、纯解释型或混合编译-解释型。事实上,大多数当前编程语言都有编译和解释版本可用。
编译和解释方法都有其优点和缺点。我将从编译语言开始。
编译语言
- 编译语言最大的优势之一是其执行速度。用 C/C++ 编写的程序比用 Java 编写的等效程序运行速度快 30-70%。
- 与解释型程序相比,编译代码占用的内存也更少。
- 缺点是,编译器比解释器更难编写。
- 编译器在调试程序方面没有提供太多帮助——您有多少次在 C 代码中收到“空指针异常”,然后花费数小时试图找出异常发生在哪一行源代码中。
- 可执行的编译代码比等效的解释代码大得多,例如,C/C++ 的 .exe 文件比等效的 Java .class 文件大得多
- 编译程序针对特定平台,因此与平台相关。
- 编译程序不允许在代码中实现安全性——例如,编译程序可以访问内存的任何区域,并可以对您的 PC 做任何它想做的事情(大多数病毒都是用编译语言制作的)。
- 由于安全性松散且依赖于平台,编译语言不特别适合用于开发互联网或基于网络的应用程序。
解释型语言
- 解释型语言提供了出色的调试支持。Java 程序员只需几分钟就能修复“空指针异常”,因为 Java 运行时不仅会指明异常的性质,还会提供异常发生的精确行号和函数调用序列(著名的堆栈跟踪信息)。这种功能是编译语言永远无法提供的。
- 另一个优点是解释器比编译器更容易构建。
- 解释器最大的优势之一是它们使平台独立性成为可能。
- 解释型语言还允许高度的安全性——这是互联网应用程序迫切需要的。
- 中间语言代码大小远小于编译后的可执行代码。
- 平台独立性和严格的安全性是使解释型语言非常适合互联网和基于 Web 的应用程序的两个最重要因素。
- 解释型语言存在一些严重的缺点。解释型应用程序占用更多的内存和 CPU 资源。这是因为要运行用解释型语言编写的程序;必须首先运行相应的解释器。解释器是复杂、智能且资源消耗大的程序,它们占用大量的 CPU 周期和 RAM。
- 由于解释型应用程序的解码-取-执行周期,它们比编译型程序慢得多。
- 解释器在运行时还进行大量的代码优化、安全违规检查;这些额外的步骤会占用更多的资源并进一步减慢应用程序的速度。
编译语言中的平台依赖性问题
C/C++ 是一种编译型语言,即它的功能与上面给出的图 1类似。尽管也存在至少一个(可能更多)C/C++ 解释器。您的 C/C++ 源文件被转换为 .obj 代码,然后链接器将其转换为可执行代码。此可执行代码可以在主机上运行。 .obj 和可执行代码都是机器平台/依赖的。 .exe 文件只能在特定硬件和特定操作系统上运行。几乎所有已知的操作系统-硬件组合都有可用的编译器。如果您在 Intel 上运行 Linux,那么所需的编译器通常作为 Linux 安装包的一部分提供。如果您在 Intel 上运行 Windows,那么您可以使用许多编译器之一,例如 Borland 的 C++ 或 Microsoft 的 C++ 编译器。同样,C/C++ 编译器也存在于 Apple Macintosh 上。所以您的 C/C++ 程序中似乎唯一可移植且平台独立的部分是实际的源代码——抱歉让您失望了!!!。即使这句话也只部分正确。您的 C/C++ 代码只有在使用 ANSI C 标准时才可移植。由于 C/C++ 的各种供应商特定扩展,您的 C/C++ 代码极不可能自动为所有平台编译。因此,如果您想确保您的代码在所有平台上编译;那么在包含任何 API 或函数之前,您应该确保它是标准而非供应商特定的。通常,C/C++/VC++ 中可用的 GUI 函数总是平台相关的。因此,您在 VC++ 中习惯的简单 MessageBox() API 将无法在 Unix 中工作。事实上,您在 VC++ 中编写的大部分代码都无法在任何其他平台上运行——甚至 Windows NT 应用程序可能无法在 Windows 2000 上运行,反之亦然。因此,尽管 C/C++ 生成了最有效的可执行文件之一——但在平台独立性方面它却功亏一篑。虽然 C/C++ 的这一缺点众所周知,但在互联网成为家庭工具之前,它并没有造成任何问题。互联网带来了需要在多个平台上运行单个应用程序而无需任何更改的需求。这时 Sun 挺身而出,开发了 Java。
Java 程序如何工作?
Java 程序员将代码写入扩展名为 .java 的文件中。源文件将导入几个 Java 框架类/包/库,例如 java.lang、java.utils 等。为了让程序员生成一个 java 文件;他/她必须在计算机上安装 JDK(Java 开发工具包)。JDK 是一套全面的软件,包括开发 Java 应用程序所需的所有零散组件。这包括 JVM(Java 虚拟机)、JRE(Java 运行时环境;实际上 JVM 是 JRE 的一部分)、Java 包和框架类、javac(Java 编译器)以及 Java 调试器。
程序完成后,程序员将使用 Java 编译器编译 Java 源代码。编译器的输出是一个 .class 文件。
因此,如果您将代码放在名为 `Test.java` 的文件中;您将使用 `javac` 程序(Java 编译器)将您的源文件编译成一个名为 `Test.class` 的类文件。
您的 `Test.java` 是一个 Java 源代码文本文件,而 `Test.class` 文件是一个中间的 Java 字节码文件,该文件实际上是独立于机器的中间代码,可以在任何安装了 JRE 的计算机上执行。
要运行您的 `Test.class` 文件,您将使用 Java 运行时环境。使用 `java` 命令运行测试文件。
上面是对如何运行 Java 程序的极其简化的讨论。但在运行 Java 程序之前,您必须设置 CLASSPATH(一个环境变量),使其指向所有引用的库/包。您还必须使用带有适当开关和参数的 `javac` 来正确编译您的 `Test.java` 文件。
基本思想是,在您的 Java 程序中,您将使用 Java 框架类/包/库,甚至第三方包(例如,`import com.wrq.apptrieve.*` 将告诉编译器您将引用此包中的类)。编译器需要知道这些包的位置才能成功编译“Test.java”。编译后,JRE 也需要访问这些外部包才能成功运行您的程序。JRE 附带了基本的框架类/包,因此 JRE 已经知道这些包;但是对于第三方/外部包,您需要通过正确设置 CLASSPATH 来告诉 JRE 在哪里找到它们。
一旦 JRE 找到所有必要的包/文件/库,它就可以运行您的程序了。
Java 平台独立性的原因在于 JRE 的普遍性。JREs 可用于大多数商业和流行平台。这对程序员意味着他/她只需要编写一次代码,相同的程序就可以在任何平台上运行。这与用 Visual C++/Visual Basic 等编写的程序不同,后者只能在目标平台上运行。
什么是 Java 虚拟机?
在我详细讨论 JVM 之前,让我澄清一些相关的术语。
- Java 开发工具包 (JDK):这包括所有基本的 Java 框架包、编译器 (javac)、JRE、JVM、调试器等,简而言之,所有您需要开发、调试、编译和运行 Java 程序的东西。
- Java 运行时环境 (JRE):这是 JDK 的子集。它不包括调试器、编译器和框架类。它包括计算机运行 `.class` 文件所需的最低限度。
- Java 虚拟机 (JVM):JVM 是 JRE 的一部分。`.class` 文件被传递给 JVM,然后 JVM 运行该程序。JRE 确保代码不违反任何安全限制。请记住,字节码(.class 文件)不会直接在主机上运行;它需要转换为主机机器的语言。这种转换由 JVM 完成。在转换过程中,JVM 会确保安全性,并且还可能优化代码。市场上有许多商用 JVM——不同的 JVM 具有不同的功能和不同程度的性能。为了以最小的延迟生成高效的代码,JVM 需要内置大量的智能。这也将使 JVM 的体积更大。请记住,要运行 Java 程序,JVM 必须加载到内存中,显然,一个大型的 JVM 需要比紧凑型 JVM 更多的计算机资源。因此,JVM 的大小和其功能之间必须有一个良好的平衡。这就是为什么 Java 程序总是比等效的 C++ 程序慢 30-70%。
最初的 JVM 极其缓慢且耗费资源——因此,当您运行 Java 程序时,您的硬盘会不断地“嘎吱作响”。近年来,许多高效的 JVM 已经浮出水面。这些 JVM 使用不同的编译技术,以尽可能少的时间生成高效的机器代码。其中一种技术称为即时 (JIT) 编译。这项技术也已应用于 .NET。
即时编译 (JIT):有关即时编译的详细讨论可在本文参考文献中找到。我将只简要讨论 JIT。
即时 (JIT) 编译器有望提高 Java 应用程序的性能。JIT 编译器不是让 JVM 运行字节码,而是将代码翻译成主机机器的本地语言。因此,应用程序在保持 Java 可移植性的同时获得了编译代码的性能增强[8]。下面是 JIT 工作原理的图示[5]

一个没有 JIT 增强的简单 JVM 会接收 Java 字节码(.class 文件),然后将指令转换为主机机器的机器码并逐条运行,这种方法的开销和延迟是显而易见的,并且已在本文中讨论过。但是当使用 JIT 时,JIT 编译器会直接将字节码 .class 文件转换为主机机器的本地机器语言并直接运行——*从而减少了开销*。如今使用的所有 JVM 都默认内置了 JIT 增强功能,如果您不希望使用 JIT,您需要在运行程序时通过使用适当的开关明确告知 JRE。
尽管 JIT 编译大大提高了程序的执行速度,但它涉及在运行时将字节码转换为本地代码的开销。正是由于这个原因,尽管有 JIT,Java 程序仍然比等效的 C/C++ 程序慢。
Java Applet 是一种特殊的 Java 程序,只能在浏览器窗口中运行。当您在网页中嵌入 Java Applet 时,浏览器会识别 Applet 标签并从指定位置下载 Applet 的字节码(.class 文件)。一旦字节码下载完成,浏览器会使用 JVM(浏览器自身包含的)来运行 Applet,确保 Applet 不会执行任何不安全的 API——主要是访问客户端机器硬件的 API。
鉴于 JVM 的概念,很明显,任何编译成 Java 字节码的编程语言都可以使用 JVM 来运行程序。我们都知道 Java 代码(.java)如何转换为字节码(.class),然后由 JVM 在主机上运行。如果我们制作一个 C++ 编译器,将 C++ 源文件(.c 或 .cpp)转换为 Java 字节码文件(.class)而不是 .obj 文件,会怎么样?理论上这是可能的,但它是否实用则完全是另一个问题。事实上,已经有许多语言的编译器可以生成 Java 字节码,然后由 JVM 运行。有关此类语言的详细信息可以在 [9] 中找到。本文贬低了微软关于 CLR 是唯一支持语言对立的平台的说法。JVM 也可以(事实上已经)被不同的语言使用。
什么是 CLR?
什么是微软的公共语言运行时(CLR)?它是 .NET 应用程序的生命线。在我描述 CLR 之前,让我们解释一下运行时的含义。运行时是程序执行的环境。因此,CLR 是一个我们可以运行已编译为 IL 的 .NET 应用程序的环境。Java 程序员熟悉 JRE(Java 运行时环境)。可以将 CLR 视为 JRE 的等价物。

上图展示了 CLR 的各个组件。让我们详细讨论每个组件。12 对此进行了深入分析。
公共类型系统 (CTS) 负责将数据类型解释为通用格式——例如,一个整数有多少字节。
第二个组件,IL 编译器接收 IL 代码并将其转换为宿主机器语言。执行支持类似于语言运行时(例如,在 VB 中,运行时是 VBRunxxx.dll;然而,使用 VB.NET,我们不再需要单独的语言运行时)。
CLR 中的安全组件确保程序集(正在执行的程序)具有执行某些功能的权限。垃圾回收器类似于 Java 中的垃圾回收器。它的功能是在对象不再使用时回收内存,从而避免内存泄漏和悬空指针。类加载器组件类似于 Java 中的类加载器。它的唯一目的是加载执行应用程序所需的类。
这是完整的图片。
程序员必须先编写源代码,然后编译它。Windows 程序员总是直接将程序编译成机器码——但对于 .NET,情况发生了变化。语言编译器将程序编译成中间语言“MSIL”或简称为“IL”(很像 Java 字节码)。IL 被馈送到 CLR,然后 CLR 会使用 IL 编译器将 IL 转换为宿主机器码。
.NET 引入了“托管代码”和“非托管代码”的概念。CLR 负责内存的分配和释放。任何试图绕过 CLR 并自行处理这些功能的代码都被认为是“不安全的”;编译器将不会编译此类代码。如果用户坚持绕过 CLR 内存管理功能,那么他必须专门使用“unsafe”和“fixed”关键字来编写此类代码(详见 C# 程序员指南)。这种代码称为“非托管”代码,与依赖 CLR 进行内存分配和释放的“托管代码”相对。
因此生成的 IL 代码存在两个主要问题。首先,它没有利用平台特定的方面来增强程序执行。(例如,如果一个平台在硬件中实现了某种复杂的图形渲染算法,那么如果游戏利用此功能,它将运行得更快;然而,由于 IL 不能是平台特定的,它无法利用这些机会)。第二个问题是 IL 不能直接在机器上运行,因为它是一种中间代码而不是机器代码。为了解决这些问题,CLR 使用了一个 IL 编译器。CLR 使用 JIT 编译器将 IL 代码编译成本机代码。在 Java 中,字节码由虚拟机 (JVM) 解释。这种解释导致 Java 应用程序运行极其缓慢。JVM 中引入 JIT 提高了执行速度。在 CLR 中,Microsoft 已经消除了虚拟机步骤。IL 代码被编译成本机机器码,根本不进行解释。对于这种编译,CLR 使用以下两种 JIT 编译器
- Econo-JIT :此编译器编译速度非常快;但它生成未经优化的代码——因此程序可能启动迅速但运行缓慢。此编译器适用于运行脚本。
- 标准 JIT:此编译器编译时间较慢;但它生成高度优化的代码。大多数情况下,CLR 会使用此编译器来运行您的 IL 代码。
- 安装时编译:此技术允许 CLR 在安装时将您的应用程序编译成本机代码。因此安装可能需要几分钟,但代码将以接近本机 C/C++ 应用程序的速度运行。
一旦您的程序被编译成主机机器代码,它就可以开始执行。在执行过程中,CLR 为您的代码提供安全和内存管理服务(除非您专门使用了非托管代码)。
结论
从上述讨论中可以看出,微软做到了它最擅长的事情。它观察了 JRE/JVM 四年;然后提出了一个更高效、更稳定的运行时环境,它建立在 JRE/JVM 的优点之上并弥补了其缺点。
那么,当您开始使用 CLR 时,您应该期待什么呢?您应该肯定地期待您的程序比等效的 Java 程序运行得更快 [11];但您的程序仍然会比等效的 C/C++ 程序——或任何其他编译成机器语言的程序——运行得慢。这是所有解释型语言都有的局限性,也是您为平台独立性付出的代价。
JVM 可用于大多数平台(因此您的 Java 程序确实是平台独立的);而 CLR(在撰写本文时)仅适用于 Microsoft Windows 平台(因此 .NET 程序并非真正平台独立,它只是承诺平台独立)。微软尚未公布任何为其他平台开发 CLR 的未来计划;尽管第三方必然会为非微软平台开发 CLR。
编者注:自本文撰写以来,微软已宣布 Rotor 项目,该项目提供了 CLR 的共享源代码实现。还有其他一些项目;其中一个是 Mono 项目,该项目正试图将 CLR 引入 Linux 平台。
从长远来看,JVM 和 CLR 将协同发展。它们将相互学习,最终结果将是健康的竞争和为最终用户提供更好的运行时环境。
参考文献
- 英特尔 P4 NetBrust 架构简介
- ............. 编写的编译器简介
- ........... 编写的编程语言 - 实现与设计
- Xiaoli Zhang & Helen Wong 关于解释型编程语言的报告
- 即时编译器中优化的设计、实现和评估
- 优化 Java 字节码, Michal Cierniak 和 Wei Li。收录于 Concurrency: Software and Practice,1997 年。
- 虚方法内联, David Detlefs 和 Ole Agesen。收录于 ECOOP 1999,http://www.di.fc.ul.pt/ecoop99
- 编译器、解释器和字节码 作者:Alan Joch
- JVM 和 CLR 作者:Jon Udell
- 即时编译器 作者:Matt Welsh
- .NET 和 J2EE 应用程序比较
- .NET 简介 作者:Kashif Manzoor