1、是什么
公共语言运行时(Common Language Runtime CLR)是一个可由多种编程语言使用的“运行时”,核心功能包括:内存管理,程序集加载,安全性、异常处理和线程同步,可由面向CLR的所有语言使用。
2、为什么
知道了CLR是什么,那么为什么要有CLR,设计出CLR的目的是什么?
公共语言运行时(Common Language Runtime, CLR)的主要目的是为了提供一个统一的、高效的、安全的运行环境,使得不同的编程语言可以在同一个平台上无缝地协同工作。
我们可以通过一个通俗的比喻来理解公共语言运行时(Common Language Runtime, CLR)的本质。想象一下,CLR就像是一个大型的现代化的厨房,而编程语言则是不同的菜系的各地厨师。
1. 厨房(CLR):
- 功能齐全:这个厨房配备了各种先进的厨具和设施,能够满足不同菜肴的烹饪需求。
- 统一标准:所有厨师都遵循一套统一的厨房规则和流程,确保食物的质量和安全。
2. 厨师(编程语言):
- 多样化的厨师:不同的厨师(如C#、VB.NET、F#等)都有自己的专长和风格,但他们都在同一个厨房里工作。
- 使用相同的工具:无论哪个厨师,他们都使用厨房提供的相同工具和设备(如烤箱、炉灶、刀具等),这些工具代表了内存管理、程序集加载、安全性等功能。
3. 食材(代码):
- 准备食材:厨师们将食材(代码)带进厨房,并根据需要进行处理。
- 烹饪过程:在厨房中,厨师们使用各种工具和设备来烹饪食材(编译和执行代码)。
4. 核心功能:
- 内存管理:厨房中的智能垃圾回收系统会自动清理不需要的食材残渣(未使用的内存),保持厨房的整洁。
- 程序集加载:厨房有一个高效的仓储系统,可以快速找到并提供所需的食材和调料(程序集和库)。
- 安全性:厨房有严格的安全措施,确保只有经过认证的厨师才能进入,并且每个厨师只能访问他们被授权的区域和工具。
- 异常处理:如果某个厨师在烹饪过程中遇到问题(如烧焦或切到手),厨房的应急系统会立即响应,帮助解决问题并防止进一步的损害。
- 线程同步:厨房中的多个厨师可以同时工作,但通过有效的协调机制(如调度器),确保他们在共享资源(如炉灶、烤箱)时不会发生冲突。
- CLR 就像是一个现代化的厨房,为不同编程语言(厨师)提供了统一的环境和工具,使它们能够高效、安全地执行任务。
- 编程语言 是在这个厨房里工作的厨师,他们使用厨房提供的先进工具和设施来完成各自的任务。
- 核心功能 如内存管理、程序集加载、安全性、异常处理和线程同步,就像是厨房中的各种系统和措施,确保一切运作顺畅。
通过这个比喻,我们可以更直观地理解 CLR 的本质和它在 .NET 环境中的作用。
理解了本质,借此我们就可以方便理解CLR带来的优势和好处,以及理解CLR的设计目的:
1. 跨语言互操作性
- 统一的类型系统:CLR 提供了一个统一的类型系统(称为 Common Type System, CTS),使得不同编程语言可以共享数据类型。例如,C# 中的一个整数类型在 CLR 中与 VB.NET 或 F# 中的整数类型是相同的。
- 通用的语言基础架构:所有面向 CLR 的语言都可以使用相同的基类库(如 .NET Framework 或 .NET Core/5+ 的 BCL - Base Class Library),这简化了代码重用和维护。
2. 自动内存管理
- 垃圾回收:CLR 提供了垃圾回收机制,自动管理内存分配和释放,减少了内存泄漏和悬空指针的问题。开发者不需要手动管理内存,从而降低了程序出错的可能性。
- 资源管理:通过
using
语句等机制,CLR 可以确保资源(如文件句柄、数据库连接等)在不再需要时被正确释放。
3. 安全性
- 代码访问安全性 (CAS):CLR 提供了一种基于策略的安全模型,可以根据代码的来源和身份限制其权限。虽然 CAS 在 .NET Core 和 .NET 5+ 中已经被弃用,但其他安全特性仍然存在。
- 类型安全:编译器和运行时检查确保类型安全性,防止常见的类型错误,如将字符串赋值给整数变量。
4. 异常处理
- 结构化的异常处理:CLR 提供了一种结构化的异常处理机制,使得错误处理更加一致和可靠。通过
try-catch-finally
语句,开发者可以捕获和处理运行时错误。
5. 性能优化
- 即时编译 (JIT):CLR 使用 JIT 编译器将中间语言(IL)代码转换成本机机器码,这使得代码可以在多种硬件平台上高效运行。
- 性能分析:CLR 提供了丰富的性能分析工具,帮助开发者识别和优化性能瓶颈。
6. 平台无关性
- 可移植性:CLR 使得应用程序可以在任何支持 .NET 的平台上运行,包括 Windows、macOS 和 Linux。这提高了代码的可移植性和跨平台能力。
7. 简化开发
- 丰富的基类库:CLR 提供了大量的基类库,涵盖了从基本的数据结构到高级功能(如网络通信、图形界面、数据库访问等),简化了开发过程。
- 开发工具支持:Visual Studio 等开发工具提供了对 CLR 的强大支持,包括智能感知、调试、性能分析等功能。
8. 线程同步和并发
- 线程管理:CLR 提供了丰富的线程管理和同步机制,使得多线程编程更加简单和安全。通过
Thread
类、Task
并发库等,开发者可以轻松实现并发操作。
做一个横向的类比,CLR与Java体系中的JVM(Java Virtual Machine)相对应,两者都是运行时环境,负责管理代码的执行、内存管理和安全性等。
相同点
-
运行时环境:
- 两者都提供了一个虚拟化的运行时环境,使得编写的代码可以在多种平台上运行。
- 都支持自动内存管理(垃圾回收)和异常处理。
-
跨语言支持:
- CLR 支持多种 .NET 语言(如 C#、VB.NET、F# 等),而 JVM 支持多种基于 JVM 的语言(如 Java、Kotlin、Scala、Groovy 等)。
-
中间语言:
- CLR 使用中间语言(Intermediate Language, IL 或 MSIL)。
- JVM 使用字节码(Bytecode)。
-
性能优化:
- 两者都使用即时编译(Just-In-Time, JIT)技术将中间语言或字节码转换成本机机器码,以提高执行效率。
-
安全性:
- 两者都提供了类型安全和一定程度的安全性检查,防止常见的编程错误。
-
库支持:
- CLR 提供了丰富的基类库(Base Class Library, BCL)。
- JVM 提供了 Java 标准库(Java Standard Library)。
不同点
-
平台依赖性:
- CLR 最初主要针对 Windows 平台设计,但随着 .NET Core 和 .NET 5+ 的推出,现在也支持 macOS 和 Linux。
- JVM 从一开始就设计为跨平台的,支持多种操作系统。
-
类型系统:
- CLR 有一个更严格的类型系统,包括值类型和引用类型的区别。
- JVM 主要处理引用类型,值类型的支持相对较弱(尽管 Java 16 引入了 Value Classes,但仍在发展中)。
-
模块化:
- CLR 在 .NET Core 和 .NET 5+ 中引入了模块化的设计,可以按需加载和卸载组件。
- JVM 也有模块化的能力,特别是通过 Java Platform Module System (JPMS) 在 Java 9 中引入。
-
反射和元数据:
- CLR 提供了丰富的反射 API 和元数据访问能力。
- JVM 也有反射 API,但在某些方面不如 CLR 丰富。
-
工具和生态系统:
- CLR 有 Visual Studio 和其他 Microsoft 工具的强大支持。
- JVM 有 Eclipse、IntelliJ IDEA 和 Maven/Gradle 等广泛使用的工具。
优劣比较
CLR 优势
-
开发工具:
- Visual Studio 是一个非常强大且集成度高的开发环境,提供了丰富的调试和性能分析工具。
-
语言互操作性:
- .NET 生态系统中的不同语言(如 C# 和 F#)之间的互操作性非常好,可以无缝地混合使用。
-
性能优化:
- CLR 的 JIT 编译器在某些情况下可以生成更高效的本机代码,尤其是在现代 .NET Core 和 .NET 5+ 中。
-
企业级支持:
- Microsoft 提供了强大的企业级支持和服务,特别是在 Windows 平台上。
CLR 劣势
-
历史上的平台依赖性:
- 虽然 .NET Core 和 .NET 5+ 解决了这个问题,但早期的 .NET Framework 主要局限于 Windows 平台。
-
学习曲线:
- 对于新开发者来说,.NET 生态系统可能比 Java 更复杂,因为有更多的概念和技术需要掌握。
JVM 优势
-
跨平台性:
- JVM 从一开始就设计为跨平台的,支持多种操作系统,这使得 Java 应用程序具有更好的可移植性。
-
成熟的生态系统:
- Java 有一个非常成熟和庞大的生态系统,有大量的库和框架可供选择。
-
广泛的社区支持:
- Java 拥有一个庞大的开发者社区,提供了大量的文档、教程和第三方资源。
-
企业应用:
- Java 在企业级应用中非常流行,特别是在大型企业和金融行业中。
JVM 劣势
-
启动时间:
- JVM 的启动时间相对较长,尤其是在加载大量类和初始化复杂应用程序时。
-
内存占用:
- JVM 通常比 CLR 占用更多的内存,尤其是在运行大型应用程序时。
-
性能波动:
- JVM 的性能可能会受到 GC 停顿的影响,虽然现代 JVM 实现已经大大减少了这种情况,但仍然存在一定的性能波动。
3、怎么用
以C#为例,我们通过Visual Studio等开发工具,编写的C#代码是怎么使用CLR的?中间经历了哪些步骤?
3.1源码编译为托管模块
以C#为例,我们编写的源码经过C# 编译器检查语法和分析源码后,编译成托管模块。托管模块是标准的32/64位Microsoft Windows可移植体(PE32/PE32+)文件,他们都需要CLR才能执行。托管模块包含四部分:
1、PE32/PE32+头:标准Windows PE文件头,类似于“公共对象文件格式”。标识文件类型,包括GUI,CUI或DLL,并包含一个时间标记来指出文件的生成时间。
2、CLR头:包含使这个模块成为托管模块的信息,头中包含要求的CLR版本,一些标志,托管模块入口方法(Main方法)的MethodDef元数据token以及模块的元数据,资源,强名称,一些标志及其他不太重要的数据项的位置/大小
3、元数据:每个托管模块都包含元数据表。主要有两种表:一种表描述源代码中定义的类型和成员,另一种描述源代码中引用的类型和成员
4、IL(中间语言)代码:编译器编译源代码时生成的代码。运行时,CLR将IL编译成本机的CPU指令,IL也称托管代码
如果把托管模块比喻为一本书。这本书就有封面、前言、目录和正文四部分。
- PE32/PE32+ 头:类似于书的封面,包含基本的文件信息,帮助操作系统加载和识别文件。
- CLR 头:类似于书的前言,提供了关于如何运行和管理该模块的指南。
- 元数据:类似于书的目录,详细描述了模块中的所有类型和成员,支持反射和跨语言互操作性。
- IL 代码:类似于书的正文,包含了应用程序的实际逻辑和功能,将在运行时被 JIT 编译器转换为本机代码。
元数据简单来说是一个数据表集合,描述模块中定义了什么,引用了什么,元数据是一些老技术的超集。编译器同时生成元数据和IL代码,二者密不可分,并嵌入最终生成的托管模块,所有元数据和它描述的IL代码永远不会失去同步。
元数据的用途:
- 元数据避免了编译时对原生C/C++头和库文件的需求,因为在实现类型/成员的IL代码文件中,已经包含了有关引用类型/成员的全部信息。编译器直接从托管模块中读取元数据
- Miscrosoft Visual Studio 用元数据帮助写代码,“智能感知”技术会解析元数据,告知你一个类型提供了那些方法、属性、事件和字段。对于方法还能告诉你需要的参数
- CLR的代码验证过程使用元数据确保代码只执行“类型安全”的操作
- 元数据允许将对象的字段序列化到内存块,将其发送到另一台机器,然后反序列化,在远程机器上重建对象状态
- 元数据允许垃圾回收器跟踪对象的生存周期。垃圾回收器能判断任何对象的类型,并从元数据知道哪个对象中的哪些字段引用了其他对象。
3.2将托管模块合并成程序集
什么是程序集?
程序集(Assembly)是 .NET 平台中的一个核心概念,它是构建和部署 .NET 应用程序的基本单元。程序集可以包含多个托管模块和资源文件,并且有一个清单(Manifest)来描述其内容和元数据。
清单是元数据表的集合,这些表描述了构成程序集的文件、程序集中的文件所实现的公开导出的类型以及与程序集关联的资源或数据文件。
编译器默认将生成的托管模块转换成程序集,换言之,C#编译器生成的是含有清单的托管模块。所以对于只有一个托管模块而且无资源文件的项目,程序集就是托管模块
3.3加载公共语言运行时
编译器生成的每个程序集可以是可执行应用程序(.exe),也可以是DLL。最终CLR管理这些程序集中的代码的执行。
可执行文件运行时,Windows检查文件头,判断需要32位还是64位进程之后,会在进程地址空间加载MSCorEE.dll的x86,x64或ARM版本。然后进程的主线调用MSCorEE.dll中定义的一个方法。这个方法初始化CLR,加载EXE程序集,在调用其入口方法(Main)。随后托管应用程序启动并运行。
3.4执行程序集的代码
托管程序集包含元数据和IL,IL是一种与CPU无关的可以被视为一种面向对象的机器语言。高级语言(例如c#)通常只公开了CLR全部功能的一个子集,而IL汇编语言允许访问CLR的全部功能。
为了执行方法,首先要把方法的IL转换成本机的CPU指令,这是CLR的JIT(just-in-time 或“即时”)编译器的职责
在方法执行前,CLR检查代码引用的所有类型,并分配一个内部数据结构来管理对引用类型的访问。在这个内部数据结构中,引用类型定义的每个方法都有一个对应的记录项。每个记录项都含有一个地址,根据此地址就可以找到方法的实现。对这个结构初始化时,CLR将每个记录项设置成指向包含在CLR内部的一个未编档函数,称该函数为JITCompiler。调用方法时,JITCompiler函数将方法的IL代码编译成本机CPU指令
JITCompiler会在定义该类型的程序集的元数据中查找被调用方法的IL。接着,JITCompiler验证IL代码,将IL编译为本机CPU指令。本机CPU指令保存到动态分配的内存中。然后,JITCompiler回到CLR为类型创建的内部数据结构,找到与被调用方法对应的那条记录,修改最初对JITCompiler的引用,使其指向内存块中包含刚刚编译好的CPU指令的地址。当此方法再次被调用时,将跳过JITCompiler函数,直接指向本机CPU指令。
让我们用一个比喻来更好地理解这些概念及其相互之间的关系。想象一下,准备一顿丰盛的大餐,这个过程与.NET开发中的各个概念有着相似之处。
源码(Source Code)
- 比喻:源码就像是你准备大餐时的食谱。它是你写下的指导步骤,告诉计算机你想要做什么。
- 实际:在编程中,源码是你用C#、F#或VB等高级语言写的代码,描述了程序的功能和逻辑。
CLR (Common Language Runtime)
- 比喻:CLR就像是厨房中的厨师,他负责按照食谱上的指示准备食物,同时确保整个烹饪过程顺利进行。
- 实际:CLR是.NET框架的一部分,它负责管理程序的执行,包括内存管理、线程管理和异常处理等。
IL (Intermediate Language)
- 比喻:IL就像是厨师根据食谱制作的半成品。这些半成品已经准备好,但还需要最后一步才能变成美味的菜肴。
- 实际:IL是源码编译后的中间语言代码,它不是直接运行在硬件上的机器码,而是需要CLR进一步解释或编译成机器码后才能执行。
元数据(Metadata)
- 比喻:元数据就像是食谱旁边附带的食材清单和烹饪说明。它告诉厨师每道菜需要哪些材料,以及如何正确地使用这些材料。
- 实际:元数据是关于类型、方法、字段等的描述信息,CLR使用这些信息来理解和操作程序中的各种元素。
托管模块(Managed Module)
- 比喻:托管模块就像是厨房中的一盘半成品菜肴。这盘菜肴已经按照食谱做好了一部分,但还没有完全完成。
- 实际:托管模块是包含IL代码和元数据的文件。它是程序集的一个组成部分,可以单独编译,但通常需要与其他模块一起组成完整的程序集。
程序集(Assembly)
- 比喻:程序集就像是你准备好的整桌大餐。它包含了所有的菜肴,可以立即上桌供客人享用。
- 实际:程序集是.NET中的基本部署单元,可以是一个DLL或EXE文件。它包含了多个托管模块,以及所有必要的元数据和资源,形成了一个完整的、可执行的应用程序或库。
反射(Reflection)
- 比喻:反射就像是厨师在准备大餐时,可以根据食谱随时查看和调整每道菜的配料和步骤。
- 实际:反射是.NET中的一种机制,允许程序在运行时动态地获取类型信息、创建对象和调用方法。
JIT 编译器(Just-In-Time Compiler)
- 比喻:JIT编译器就像是厨房中的快速加热设备,可以在最后一刻将半成品菜肴迅速加热,使其达到最佳的食用状态。
- 实际:JIT编译器是CLR的一部分,它在程序运行时将IL代码编译成机器码,从而提高执行效率。
资源(Resources)
- 比喻:资源就像是厨房中的调料和装饰品,它们虽然不是主菜的一部分,但能大大提升菜肴的风味和外观。
- 实际:资源是程序集中包含的非代码数据,如图像、字符串、配置文件等,它们用于增强应用程序的功能和用户体验。