CLR,通用语言运行时,每个.Net 程序猿,都会第一时间接触到。记得2008年,第一次学习Jeffrey Richter的CLR Via C#,读的懵懵懂懂,大抵因为编码太少,理解的只是概念和皮毛。10年之后,再次找出Jeffrey Richter的CLR Via C#这本书,重读CLR。归纳总结,同时加深自我的底层技术理解和深度。分享给大家自己的总结笔记:

讲在前面的话

合抱之木,生于毫末;九层之台,起于垒土!

整个.Net 大厦建筑的底层基础技术就是CLR,通用语言运行时。CLR给我们带来JIT、垃圾回收、MSIL、Meta Data、Application Domain等一系列概念,它们共同协作,合力打造了一个与非托管代码完全不同的一个新的开发环境。每个组件如何和谐地与其他组件协作,平稳地运行.Net 应用,只有深入了解CLR,才能“拨开云雾见天日,守得云开见月明”!

总结Part1:CLR是什么? 我们的源码如何被编译成托管模块的?

  CLR:公共语言运行时(Common Language Runtime,CLR),一个可以由多种不同编程语言使用的运行时。

Mircosoft面向CLR提供了几种编程语言编译器,包括:C#、VB、J#、C++、Jscript以及一个中间语言(Intermediate Language,IL)等编译器。因此,我们可以使用支持CLR的任何编程语言来创建源代码文件,实现实际的业务逻辑。

创建源代码文件之后,要使用一个相对应的编译器来检查语法和分析源码,最后编译成一个“托管模块”。

托管模块是一个标准的32位Microsoft Windows可移植执行体(PE32)文件,后者是一个标准的64位Microsoft Windows可移植执行体(PE32+)文件,这些文件需要CLR才可以执行。

将源代码编译为托管模块:

所有CLR支持的编译器通过编译生成的都是“中间语言(Intermediate Language,IL)代码”,IL代码有时也称为托管代码,因为CLR会管理它的执行。除了生成IL,编译器还会在每个托管模块中生成完整的元数据,元数据Meta Data是一系列特殊的数据表,描述了模块中定义的内容,比如类型及成员。同时元数据表还记录了当前托管模块引用的内容,比如引用的类型及成员。

托管模块由哪几个部分组成呢?

总结Part2:将托管代码编译为程序集

将源代码编译为托管模块之后,CLR实际上并不和托管模块一起工作,相反,CLR与程序集一起工作。

程序集(assembly)是一个抽象的概念,是一个或者多个模块/资源文件的逻辑性分组。同时,程序集是一个最小的重用、安全性以及版本控制单元。

默认情况下,将托管模块和源文件转换成一个程序集的工作由编译器完成。

总结Part3:加载CLR

我们编译的每个程序集既可以是一个可执行的应用程序,也可以是一个DLL(动态链接库,其中含有一系列有可执行程序使用的类型)。最终由CLR来管理这些程序集中的代码的执行。这就要求主机上必须安装.Net Framework.

在了解CLR具体如何加载之前,我们先了解下程序集的32位和64位版本问题,及支持X86和X64 Windows平台。在Visual Studio的Project属性Build选项中,可以选择Target Platform,如果选择X86,C#编译器生成的程序集包含一个PE32头,如果选择X64,将包含PE32+头。运行一个可执行文件时,Windows会检查这个EXE文件的头,判断应用程序需要的是32位地址空间,还是64位地址空间,具有PE32头的文件可以在32位和64位地址空间中运行,具有PE32+头的文件则要求一个64位地址空间。

以下总结了C#编译器指定不同的Platform选项,会得到什么托管模块,以及运行的Windows平台:

Windows会检查EXE文件头,判断是32位进程,还是64位进程,或者Wow64进程之后,Windows在进程的地址空间中加载MSCoreEE.dll的X86、X64或者IA64版本,进程的主线程会调用MSCoreEE.dll内部定义的一个方法,这个方法会初始化CLR,加载EXE程序集,然后调用其入口方法(Main)方法。随即,被托管的应用程序将启动并运行。

如果一个非托管应用程序通过LoadLibrary来加载一个托管的程序集,Windows会加载并初始化CLR来处理包含在程序集中的代码。

总结Part4:代码执行

前面我们介绍了,托管程序集同时包含元数据和中间语言(IL),IL是一种与CPU无关的机器语言

IL比大多数CPU机器语言要高级的多,IL能访问和操作对象类型,并提供相应的指令来创建和初始化对象,在对象上调用虚方法,并能直接操作数组元素,甚至提供了用于抛出异常和捕获异常的指令,以实现错误处理。因此IL可以被看作是一种面向对象的机器语言。

开发人员使用C#、VB、C++等高级语言来编程实现业务逻辑,这些高级语言的编译器会将源代码编译为IL。

为了执行具体的某一个方法,这个方法对应的IL首先必须转换为本地CPU指令。这是CLR的JIT(Just-in-time)即时编译器的职责。

上面这段代码是如何执行的?

在Main方法执行之前,CLR会检测出Main的代码引用的所有类型,CLR会分配一个内部数据结构,用于管理对引用类型的访问

示例代码中,Main方法引用了单一类型Console,CLR为Console分配了一个单独的内部数据结构,Console类型中每个方法都对应一条记录,每条记录都容纳了一个地址,根据这个地址就可以找到方法的实现。CLR对这个内部数据结构初始化的时候,每条记录都设置成CLR内部包含的一个未文档化的函数,这个函数称为JITCompiler

Main方法首次调用WriteLine时,会调用JITCompiler函数,JITCompiler函数负责将这个方法的IL代码编译为本地CPU指令,由于IL是“即时”编译的,所以通常将CLR这个即时编译组件称为JIT编译器

第一次调用Console.WriteLine这个方法时:

  1. JITCompiler函数被调用时,它知道要调用哪个方法,以及具体的类型定义了该方法。
  2. 然后,JITCompiler会在程序集的元数据中搜索被调用方法的IL。
  3. JITCompiler验证IL代码,并将IL代码编译成本地CPU指令,同时本地CPU指令保存在一个动态分配的内存中。
  4. 然后,JITCompiler回到CLR为类型创建的内部数据结构中,找到与被调用方法对应的那条记录,将最初调用它的那个引用替换成CPU指令内存块的地址。
  5. 最后,JITCompiler函数会跳转到内存块中的代码(CPU指令),即Console.WriteLine方法的具体实现。
  6. 代码执行完毕后,返回到Main方法中,继续执行其他代码。

       Main函数第二次调用Console.WriteLine方法

第二次调用时,由于已经对WriteLine方法进行了验证和编译,所以会直接执行内存块中代码(CPU指令),完全跳过了JITCompiler函数。因此,一个方法只有在第一次调用时才会造成一定的性能消耗。后续对此方法的调用都以本地代码的方法全速运行。

总结:

1. 一个方法只有在第一次调用时的JIT编译才会造成一定的性能消耗。后续对此方法的调用都以本地代码的方法全速运行

2. JIT编译器将本地CPU指令保存在动态内存中,一旦应用程序终止,编译好的本地CPU指令代码会被丢弃。所以,如果关闭重新运行应用程序,或者同时启动了应用程序的两个实例(两个不同的进程),JIT编译器必须再次将IL编译成本地CPU指令.

3. 对于大多数应用程序来说,因JIT编译造成的性能损失并不明显,大多数应用程序倾向于反复调用相同的方法。

4. 同时,CLR的JIT编译器会对本地代码进行优化。代码在优化后将获得更出色的性能。

先临时写到这,10年后重读CLR Via C#,更多的收获是,对.Net 底层技术原理的理解更深,同时有了更敬畏之心。后续计划再重读更多内容,分享给大家。

知识在与分享和总结!

周国庆

2018/6/9

知识在与温故、总结-再读CLR的更多相关文章

  1. 再读《Java编程思想 》

    前段时间在豆瓣上无意间看到一个帖子"我为什么把thinking in java 读了10遍",是11年的帖子,下面评论至今,各种声音都有,不过大多数还是佩服和支持的.我个人来讲也是 ...

  2. 再读《C++ Primer》——变量和基本类型

    刚上大学那时,几个室友一块买了本<C++ Primer>第4版,看了一遍后就没怎么碰了,偶尔拿出来翻翻,当作工具书使用.后来知道有第5版了,一直觉得内容差不多吧.直到最近,再读其中的一些内 ...

  3. 再读GFS论文

    http://loopjump.com/gfs_paper_note/ 再读GFS的一些笔记.主要涉及GFS架构.Chunk大小选择的一些折中考量.元数据管理及锁.写数据流程.GFS一致性模型的理解. ...

  4. 再读Android sqlite

    再读Android sqlite Android原生支持sqlite数据库操作,sqlite时轻量级关系型数据库,支持标准sql语句.Android对sqlite进行良好的接口封装来避免sql注入等安 ...

  5. 再谈CLR查找和加载程序集的方式

    原文:再谈CLR查找和加载程序集的方式 这是一个老问题,以前也有朋友写过一些文章介绍,但可能还不是很全面.我也多次被人问到,这里结合案例再次谈谈,希望对大家有所帮助. 本文范例代码可以通过这里下载 h ...

  6. 吃透Javascript数组操作的正确姿势—再读《Js高程》

    Javascript中关于数组对象的操作方法比较多也比较杂,正好再次捡起<Javascript高级程序设计>来读,把它们一一总结梳理了一下: 方法类别 方法名称 方法描述 参数 返回值 备 ...

  7. 再读TCP/IP网络7层协议

    随着工作的深入,每次读这7层协议,每次都有不同的理解. 分层名                                                               分层号   ...

  8. 再读vue2.0

    玩过一段时间后在来读读vue2.0会发现受益良多 概述: vue2.0 是一套构建用户界面的渐进式框架, 使用virtual DOM  提供了响应式和组件化, 允许使用简介的模板语法来声明式的将数据渲 ...

  9. 【源码分析】HashMap源码再读-基于Java8

    最近工作不是太忙,准备再读读一些源码,想来想去,还是先从JDK的源码读起吧,毕竟很久不去读了,很多东西都生疏了.当然,还是先从炙手可热的HashMap,每次读都会有一些收获.当然,JDK8对HashM ...

随机推荐

  1. tomcat 配置 使用综合

    [参考]Tomcat 7.0安装与配置 [参考]tomcat 控制台日志(startup.bat)输出到指定文件中 [参考]将Java web应用部署到Tomcat 及部署到Tomcat根目录 的三种 ...

  2. easyui的tree节点的获取和选中

    1.设置选中tree的节点 var node = $('#tt').tree('find', 1);//找到id为”tt“这个树的节点id为”1“的对象$('#tt').tree('select', ...

  3. 手写一个selenium浏览器池

    维护一组浏览器,实现每分钟1000次查询.DriverPool使用变幻版只初始化一次的单例模式.维护每个浏览器的当前是否使用的状态. 不需要等待请求来了,临时开浏览器,开一个浏览器会耽误6秒钟. 可以 ...

  4. MSF实现RID劫持和MSF实现PsExec执行命令

    msf实现rid劫持 rid劫持原理: 每个帐户都有一个指定的RID来标识它.与域控制器不同,Windows工作站和服务器会将大部分数据存储在HKLM\SAM\SAM\Domains\Account\ ...

  5. maven的安装及试用

    安装包准备: jdk-7u79-linux-x64.rpmapache-maven-3.5.3-bin.tar.gz 安装: rpm -ihv jdk-7u79-linux-x64.rpmtar -x ...

  6. js设计模式(五)---观察者模式

    概述: 观察者模式也叫 “ 发布-订阅 " 模式 , 发布者发布信息是不需要考虑订阅者是谁?添加订阅者的时候也不需要通知发布者. 应用: 最经典的就是: DOM事件 开发过程中我们常用自定义 ...

  7. Oracle课程档案,第三天

    count(*):有多少行,对行做统计 count(x):列.... sum:和 avg:平均值 min:求最小值 max:求最大值 distinct:取出重复的值 count:计数 group by ...

  8. Oracle课程档案,第二天

    salary:工资 order by:排序 desc:降序 hire:雇佣 单行函数 一周有七天 一月不一定只有30天 trunc:截取 dual:空表 last:最后 month:月份 round: ...

  9. Android 学习书籍下载

    链接:https://pan.baidu.com/s/1Y6LHLJlYDfbNjoMAVjfjMw               密码:ywbk 链接:https://pan.baidu.com/s/ ...

  10. C#遍历枚举(Enum)值

    foreach (object o in Enum.GetValues(typeof(EmpType))) { Console.WriteLine("{0}:{1}", o, En ...