知識在與溫故、總結-再讀CLR

編程

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才能夠執行。ui

  將源代碼編譯爲託管模塊:

  

  全部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的X8六、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

相關文章
相關標籤/搜索