這是一篇摘抄的文章 git
有一些內容對我頗有幫助 、有一些內容解釋很清晰 因此我拿過來了。github
第一遍用了5天時間,第二遍看的時候決定本身複製一份出來因而有了這兒博客。web
什麼是.NET?什麼是.NET Framework?本文將從上往下,按部就班的介紹一系列相關.NET的概念,先從類型系統開始講起,我將經過跨語言操做這個例子來逐漸引入一系列.NET的相關概念,這主要包括:CLS、CTS(CLI)、FCL、Windows下CLR的相關核心組成、Windows下託管程序運行概念、什麼是.NET Framework,.NET Core,.NET Standard及一些VS編譯器相關雜項和相關閱讀連接。完整的從上讀到下則你能夠理解個大概的.NET體系。算法
語言,是人們進行溝通表達的主要方式。編程語言,是人與機器溝通的表達方式。不一樣的編程語言,其側重點不一樣。編程
微軟公司是全球最大的電腦軟件提供商,爲了佔據開發者市場,進而在2002年推出了Visual Studio(簡稱VS,是微軟提供給開發者的工具集) .NET 1.0版本的開發者平臺。而爲了吸引更多的開發者涌入平臺,微軟還在2002年宣佈推出一個特性強大而且與.NET平臺無縫集成的編程語言,即C# 1.0正式版。
只要是.NET支持的編程語言,開發者就能夠經過.NET平臺提供的工具服務和框架支持便捷的開發應用程序。c#
C#就是爲宣傳.NET而創立的,它直接集成於Visual Studio .NET中,VB也在.NET 1.0發佈後對其進行支持, 因此這兩門語言與.NET平臺耦合度很高,而且.NET上的技術大多都是以C#編程語言爲示例,因此常常就.NET和C#混爲一談(實質上它們是相輔相成的兩個概念)。windows
跨語言:即只要是面向.NET平臺的編程語言((C#、Visual Basic、C++/CLI、Eiffel、F#、IronPython、IronRuby、PowerBuilder、Visual COBOL 以及 Windows PowerShell)),用其中一種語言編寫的類型能夠無縫地用在另外一種語言編寫的應用程序中的互操做性。
跨平臺:一次編譯,不須要任何代碼修改,應用程序就能夠運行在任意有.NET框架實現的平臺上,即代碼不依賴於操做系統,也不依賴硬件環境。api
每門語言在最初被設計時都有其在功能和語法上的定位,讓不一樣的人使用擅長的語言去幹合適的事,這在團隊協做時尤其重要。
.NET平臺上的跨語言是經過CLS這個概念來實現的,它是一組語言互操做的標準規範,它就是公共語言規範 - Common Language Specification ,簡稱CLS;數組
CLS從類型、命名、事件、屬性、數組等方面對語言進行了共性的定義及規範。這些東西被提交給歐洲計算機制造聯合會ECMA,稱爲:共同語言基礎設施。緩存
假設你已經圍繞着封裝 繼承 多態 這3個特性設計出了多款面向對象的語言,你發現你們都是面向對象,都能很好的將現實中的對象模型表達出來。除了語法和功能擅長不一樣,語言的定義和設計結構其實都差很少一回事。
現實中你看到了一輛小汽車,這輛車裏坐着兩我的,那麼如何用這門語言來表達這樣的一個概念和場面?
首先要爲這門語言橫向定義一個「類型」的概念。接下來在程序中就能夠這樣表示:有一個汽車類型,有一我的類型,在一個汽車類型的對象內包含着兩我的類型的對象,由於要表達出這個模型,你又引入了「對象」的概念 。而如今,你又看到,汽車裏面的人作出了開車的這樣一個動做,由此你又引入了「動做指令」這樣一個概念。
接着,你又恍然大悟總結出一個定理,不管是什麼樣的「類型」,都只會存在這樣一個特徵,即活着的 帶生命特徵的(如人) 和 死的 沒有生命特徵的(如汽車) 這二者中的一個。最後,隨着思想模型的成熟,你發現,這個「類型」就至關於一個富有主體特徵的一組指令的集合。
好,而後你開始照葫蘆畫瓢。
總結出了一門語言不少必要的東西如兩種主要類別:值類別和引用類別,五個主要類型:類、接口、委託、結構、枚舉,我還規定了,一個類型能夠包含字段、屬性、方法、事件等成員,我還指定了每種類型的可見性規則和類型成員的訪問規則,等等等等,只要按照我這個體系來設計語言,設計出來的語言它可以擁有不少不錯的特性,好比跨語言,跨平臺等,C#和VB.net之因此可以這樣就是由於這兩門語言的設計符合我這個體系。
當你須要設計面向.Net的語言時所須要遵循一個體系(.Net平臺下的語言都支持的一個體系)這個體系就是CTS(Common Type System 公共類型系統),它包括但不限於:
上文的CLS是CTS(Common Type System 公共類型系統)這個體系中的子集。
一個編程語言,若是它可以支持CTS,那麼咱們就稱它爲面向.NET平臺的語言。
微軟已經將CTS和.NET的一些其它組件,提交給ECMA以成爲公開的標準,最後造成的標準稱爲CLI(Common Language Infrastructure)公共語言基礎結構。
因此有的時候你見到的書籍或文章有的只提起CTS,有的只提起CLI,請不要奇怪,你能夠寬泛的把他們理解成一個意思,CLI是微軟將CTS等內容提交給國際組織計算機制造聯合會ECMA的一個工業標準。
在CTS中有一條就是要求基元數據類型的類庫。咱們先搞清什麼是類庫?類庫就是類的邏輯集合,你開發工做中你用過或本身編寫過不少工具類,好比搞Web的常常要用到的 JsonHelper、XmlHelper、HttpHelper等等,這些類一般都會在命名爲Tool、Utility等這樣的項目中。 像這些類的集合咱們能夠在邏輯上稱之爲 "類庫",好比這些Helper咱們統稱爲工具類庫。
控制檯中你直接就能夠用ConSole類來輸出信息,或者using System.IO 便可經過File類對文件進行讀取或寫入操做,這些類都是微軟幫你寫好的,不用你本身去編寫,它幫你編寫了一個面向.NET的開發語言中使用的基本的功能,這部分類,咱們稱之爲BCL(Base Class Library), 基礎類庫,它們大多都包含在System命名空間下。
基礎類庫BCL包含:基本數據類型,文件操做,集合,自定義屬性,格式設置,安全屬性,I/O流,字符串操做,事件日誌等的類型
Framework Class Library ,.NET框架類庫,我上述所表達的BCL就是FCL中的一個基礎部分,FCL中大部分類都是經過C#來編寫的。
在FCL中,除了最基礎的那部分BCL以外,還包含咱們常見的 如 : 用於網站開發技術的 ASP.NET類庫,該子類包含webform/webpage/mvc,用於桌面開發的 WPF類庫、WinForm類庫,用於通訊交互的WCF、asp.net web api、Web Service類庫等等
每門語言都會定義一些基礎的類型,好比C#經過 int 來定義整型,用 string 來定義 字符串 ,用 object 來定義 根類。
CTS定義的一個很是重要的規則,就是類與類之間只能單繼承,System.Object類是全部類型的根,任何類都是顯式或隱式的繼承於System.Object。
什麼是.NET的跨平臺,並解釋爲何可以跨語言。不過要想知道什麼是跨平臺,首先你得知道一個程序是如何在本機上運行的。
CPU,全稱Central Processing Unit,叫作中央處理器,它是一塊超大規模的集成電路,
CPU是一臺計算機的運算核心和控制核心,CPU從存儲器或高速緩衝存儲器中取出指令,放入指令寄存器,並對指令譯碼,執行指令。
咱們運行一個程序,CPU就會不斷的讀取程序中的指令並執行,直到關閉程序。事實上,從電腦開機開始,CPU就一直在不斷的執行指令直到電腦關機。
在計算機角度,每一種CPU類型都有本身能夠識別的一套指令集,計算機無論你這個程序是用什麼語言來編寫的,其最終只認其CPU可以識別的二進制指令集。
在早期計算機剛發展的時代,人們都是直接輸入01010101這樣的沒有語義的二進制指令來讓計算機工做的,可讀性幾乎沒有,沒人願意直接編寫那些沒有可讀性、繁瑣、費時,易出差錯的二進制01代碼,因此後來纔出現了編程語言。
編程語言的誕生,使得人們編寫的代碼有了可讀性,有了語義,與直接用01相比,更有利於記憶。
而前面說了,計算機最終只識別二進制的指令,那麼,咱們用編程語言編寫出來的代碼就必需要轉換成供機器識別的指令。
一門編程語言所編寫的代碼文件轉換成能讓本機識別的指令,這中間是須要一個翻譯的過程。這個翻譯過程是須要工具來完成,咱們把它叫作 編譯器。
不一樣廠商的CPU有着不一樣的指令集,爲了克服面向CPU的指令集的難讀、難編、難記和易出錯的缺點,後來就出現了面向特定CPU的特定彙編語言
不一樣CPU架構上的彙編語言指令不一樣,而爲了統一一套寫法,同時又不失彙編的表達能力,C語言就誕生了。
C語言寫的代碼文件,會被C編譯器先轉換成對應平臺的彙編指令,再轉成機器碼,最後將這些過程當中產生的中間模塊連接成一個能夠被操做系統執行的程序。
彙編語言和C語言比較,咱們就不須要去閱讀特定CPU的彙編碼,我只須要寫通用的C源碼就能夠實現程序的編寫,咱們用將更偏機器實現的彙編語言稱爲低級語言,與彙編相比,C語言就稱之爲高級語言。
C#,咱們在編碼的時候都不須要過於偏向特定平臺的實現,翻譯過程也基本遵循這個過程。它的編譯模型和C語言相似,都是屬於這種間接轉換的中間步驟,故而可以跨平臺。
因此就相似於C/C#等這樣的高級語言來講是不區分平臺的,而在於其背後支持的這個 翻譯原理 是否能支持其它平臺。
.NET不只提供了自動內存管理的支持,他還提供了一些列的如類型安全、應用程序域、異常機制等支持,這些 都被統稱爲CLR公共語言運行庫。
CLR是.NET類型系統的基礎,全部的.NET技術都是創建在此之上,熟悉它能夠幫助咱們更好的理解框架組件的核心、原理。
在咱們執行託管代碼以前,總會先運行這些運行庫代碼,經過運行庫的代碼調用,從而構成了一個用來支持託管程序的運行環境,進而完成諸如不須要開發人員手動管理內存,一套代碼便可在各大平臺跑的這樣的操做。
這套環境及體系之完善,以致於就像一個小型的系統同樣,因此一般形象的稱CLR爲".NET虛擬機"。
若是以進程爲最低端,進程的上面就是.NET虛擬機(CLR),而虛擬機的上面纔是咱們的託管代碼。換句話說,託管程序其實是寄宿於.NET虛擬機中。
編譯器,即將源代碼文件給翻譯成一個計算機可識別的二進制程序。而在.NET Framework目錄文件夾中就附帶的有 用於C#語言的命令行形式的編譯器csc.exe 和 用於VB語言的命令行形式的編譯器vbc.exe。
經過編譯器能夠將後綴爲.cs(C#)和.vb(VB)類型的文件編譯成程序集。
程序集是一個抽象的概念,不一樣的編譯選項會產生不一樣形式的程序集。以文件個數來區分的話,那麼就分 單文件程序集(即一個文件)和多文件程序集(多個文件)。
而不管是單文件程序集仍是多文件程序集,其總有一個核心文件,就是表現爲後綴爲.dll或.exe格式的文件。
它們都是標準的PE格式的文件,主要由4部分構成:
如今咱們已經有了一個demo.exe的可執行程序,它是如何被咱們運行的?。
C#源碼被編譯成程序集,程序集內主要是由一些元數據表和IL代碼構成,咱們雙擊執行該exe,Windows加載器將該exe(PE格式文件)給映射到虛擬內存中,程序集的相關信息都會被加載至內存中,並查看PE文件的入口點(EntryPoint)並跳轉至指定的mscoree.dll中的_CorExeMain函數,該函數會執行一系列相關dll來構造CLR環境,當CLR預熱後調用該程序集的入口方法Main(),接下來由CLR來執行託管代碼(IL代碼)。
前面說了,計算機最終只識別二進制的機器碼,在CLR下有一個用來將IL代碼轉換成機器碼的引擎,稱爲Just In Time Compiler,簡稱JIT,CLR老是先將IL代碼按需經過該引擎編譯成機器指令再讓CPU執行,在這期間CLR會驗證代碼和元數據是否類型安全(在對象上只調用正肯定義的操做、標識與聲稱的要求一致、對類型的引用嚴格符合所引用的類型),被編譯過的代碼無需JIT再次編譯,而被編譯好的機器指令是被存在內存當中,當程序關閉後再打開仍要從新JIT編譯。
CLR的內嵌編譯器是即時性的,這樣的一個很明顯的好處就是能夠根據當時本機狀況生成更有利於本機的優化代碼,但一樣的,每次在對代碼編譯時都須要一個預熱的操做,它須要一個運行時環境來支持,這之間仍是有消耗的。
而與即時編譯所對應的,就是提早編譯了,英文爲Ahead of Time Compilation,簡稱AOT,也稱之爲靜態編譯。
在.NET中,使用Ngen.exe或者開源的.NET Native能夠提早將代碼編譯成本機指令。
Ngen是將IL代碼提早給所有編譯成本機代碼並安裝在本機的本機映像緩存中,故而能夠減小程序因JIT預熱的時間,但一樣的也會有不少注意事項,好比因JIT的喪失而帶來的一些特性就沒有了,如類型驗證。Ngen僅是儘量代碼提早編譯,程序的運行仍須要完整的CLR來支持。
.NET Native在將IL轉換爲本機代碼的時候,會嘗試消除全部元數據將依靠反射和元數據的代碼替換爲靜態本機代碼,而且將完整的CLR替換爲主要包含垃圾回收器的重構運行時mrt100_app.dll。
對於自身程序集內定義的類型,咱們能夠直接從自身程序集中的元數據中獲取,對於在其它程序集中定義的類型,CLR會經過一組規則來在磁盤中找到該程序集並加載在內存。
CLR在查找引用的程序集的位置時候,第一個判斷條件是 判斷該程序集是否被簽名。
什麼是簽名?
就好比你們都叫張三,姓名都同樣,喊一聲張三不知道到底在叫誰。這時候咱們就必須擴展一下這個名字以讓它具備惟一性。
咱們能夠經過sn.exe或VS對項目右鍵屬性在簽名選項卡中採起RSA算法對程序集進行數字簽名(加密:公鑰加密,私鑰解密。簽名:私鑰簽名,公鑰驗證簽名),會將構成程序集的全部文件經過哈希算法生成哈希值,而後經過非對稱加密算法用私鑰簽名,最後公佈公鑰生成一串token,最終將生成一個由程序集名稱、版本號、語言文化、公鑰組成的惟一標識,它至關於一個強化的名稱,即強名稱程序集。
mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
咱們平常在VS中的項目默認都沒有被簽名,因此就是弱名稱程序集。強名稱程序集是具備惟一標識性的程序集,而且能夠經過對比哈希值來比較程序集是否被篡改,不過仍然有不少手段和軟件能夠去掉程序集的簽名。
須要值得注意的一點是:當你試圖在已生成好的強名稱程序集中引用弱名稱程序集,那麼你必須對弱名稱程序集進行簽名並在強名稱程序集中從新註冊。
之因此這樣是由於一個程序集是否被篡改還要考慮到該程序集所引用的那些程序集,根據CLR搜索程序集的規則(下文會介紹),沒有被簽名的程序集能夠被隨意替換,因此考慮到安全性,強名稱程序集必須引用強名稱程序集,不然就會報錯:須要強名稱程序集。
事實上,按照存儲位置來講,程序集分爲共享(全局)程序集和私有程序集。
CLR查找程序集的時候,會先判斷該程序集是否被強簽名,若是強簽名了那麼就會去共享程序集的存儲位置(後文的GAC)去找,若是沒找到或者該程序集沒有被強簽名,那麼就從該程序集的同一目錄下去尋找。
強名稱程序集是先找到與程序集名稱(VS中對項目右鍵屬性應用程序->程序集名稱)相等的文件名稱,而後 按照惟一標識再來確認,確認後CLR加載程序集,同時會經過公鑰效驗該簽名來驗證程序集是否被篡改(若是想跳過驗證可查閱https://docs.microsoft.com/zh-cn/dotnet/framework/app-domains/how-to-disable-the-strong-name-bypass-feature),若是強名稱程序集被篡改則報錯。
而弱名稱程序集則直接按照與程序集名稱相等的文件名稱來找,若是仍是沒有找到就以該程序集名稱爲目錄的文件夾下去找。總之,若是最終結果就是沒找到那就會報System.IO.FileNotFoundException異常,即嘗試訪問磁盤上不存在的文件失敗時引起的異常。
注意:此處文件名稱和程序集名稱是兩個概念,不要模棱兩可,文件CLR頭內嵌程序集名稱。
項目間的生成是有序生成的,它取決於項目間的依賴順序。
好比Web項目引用BLL項目,BLL項目引用了DAL項目。那麼當我生成Web項目的時候,由於我要註冊Bll程序集,因此我要先生成Bll程序集,而BLL程序集又引用了Dal,因此又要先生成Dal程序集,因此程序集生成順序就是Dal=>BLL=>Web,項目越多編譯的時間就越久。
程序集之間的依賴順序決定了編譯順序,因此在設計項目間的分層劃分時不只要體現出層級職責,還要考慮到依賴順序。代碼存放在哪一個項目要有講究,不容許出現互相引用的狀況,好比A項目中的代碼引用B,B項目中的代碼又引用A。
而除了注意編譯順序外,咱們還要注意程序集間的版本問題,版本間的錯亂會致使程序的異常。
舉個經典的例子:Newtonsoft.Json的版本警告,大多數人都知道經過版本重定向來解決這個問題,但不多有人會琢磨爲何會出現這個問題,找了一圈文章,沒找到一個解釋的。
好比:
A程序集引用了 C盤:\Newtonsoft.Json 6.0程序集
B程序集引用了 從Nuget下載下來的Newtonsoft.Json 10.0程序集
此時A引用B,就會報:發現同一依賴程序集的不一樣版本間存在沒法解決的衝突 這一警告。
A:引用Newtonsoft.Json 6.0 Func() { var obj= Newtonsoft.Json.Obj; B.JsonObj(); } B: 引用Newtonsoft.Json 10.0 JsonObj() { return Newtonsoft.Json.Obj; }
A程序集中的Func方法調用了B程序集中的JsonObj方法,JsonObj方法又調用了Newtonsoft.Json 10.0程序集中的對象,那麼當執行Func方法時程序就會異常,報System.IO.FileNotFoundException: 未能加載文件或程序集Newtonsoft.Json 10.0的錯誤。
這是爲何?
1.這是由於依賴順序引發的。A引用了B,首先會先生成B,而B引用了 Newtonsoft.Json 10.0,那麼VS就會將源引用文件(Newtonsoft.Json 10.0)複製到B程序集同一目錄(bin/Debug)下,名爲Newtonsoft.Json.dll文件,其內嵌程序集版本爲10.0。
2.而後A引用了B,因此會將B程序集和B程序集的依賴項(Newtonsoft.Json.dll)給複製到A的程序集目錄下,而A又引用了C盤的Newtonsoft.Json 6.0程序集文件,因此又將C:\Newtonsoft.Json.dll文件給複製到本身程序集目錄下。由於兩個Newtonsoft.Json.dll重名,因此直接覆蓋了前者,那麼只保留了Newtonsoft.Json 6.0。
3.當咱們調用Func方法中的B.Convert()時候,CLR會搜索B程序集,找到後再調用 return Newtonsoft.Json.Obj 這行代碼,而這行代碼又用到了Newtonsoft.Json程序集,接下來CLR搜索Newtonsoft.Json.dll,文件名稱知足,接下來CLR判斷其標識,發現版本號是6.0,與B程序集清單裏註冊的10.0版本不符,故而纔會報出異常:未能加載文件或程序集Newtonsoft.Json 10.0。
以上就是爲什麼Newtonsoft.Json版本不一致會致使錯誤的緣由,其也詮釋了CLR搜索程序集的一個過程。
那麼,若是我執意如此,有什麼好的解決方法能讓程序順利執行呢?有,有2個方法。
第一種:經過bindingRedirect節點重定向,即當找到10.0的版本時,給定向到6.0版本
<runtime> <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1"> <dependentAssembly> <assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral" /> <bindingRedirect oldVersion="10.0.0.0" newVersion="6.0.0.0" /> </dependentAssembly> </assemblyBinding> </runtime>
注意:我看過有的文章裏寫的一個AppDomain只能加載一個相同的程序集,不少人都覺得不能同時加載2個不一樣版本的程序集,實際上CLR是能夠同時加載Newtonsoft.Json 6.0和Newtonsoft.Json 10.0的。
第二種:對每一個版本指定codeBase路徑,而後分別放上不一樣版本的程序集,這樣就能夠加載兩個相同的程序集。
<runtime> <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1"> <dependentAssembly> <assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral" /> <codeBase version="6.0.0.0" href="D:\6.0\Newtonsoft.Json.dll" /> </dependentAssembly> <dependentAssembly> <assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral" /> <codeBase version="10.0.0.0" href="D:\10.0\Newtonsoft.Json.dll" /> </dependentAssembly> </assemblyBinding> </runtime>
項目間的生成是有序生成的,它取決於項目間的依賴順序。
好比Web項目引用BLL項目,BLL項目引用了DAL項目。那麼當我生成Web項目的時候,由於我要註冊Bll程序集,因此我要先生成Bll程序集,而BLL程序集又引用了Dal,因此又要先生成Dal程序集,因此程序集生成順序就是Dal=>BLL=>Web,項目越多編譯的時間就越久。
項目間的生成是有序生成的,它取決於項目間的依賴順序。
好比Web項目引用BLL項目,BLL項目引用了DAL項目。那麼當我生成Web項目的時候,由於我要註冊Bll程序集,因此我要先生成Bll程序集,而BLL程序集又引用了Dal,因此又要先生成Dal程序集,因此程序集生成順序就是Dal=>BLL=>Web,項目越多編譯的時間就越久。
程序集之間的依賴順序決定了編譯順序,因此在設計項目間的分層劃分時不只要體現出層級職責,還要考慮到依賴順序。代碼存放在哪一個項目要有講究,不容許出現互相引用的狀況,好比A項目中的代碼引用B,B項目中的代碼又引用A。
程序集之間的依賴順序決定了編譯順序,因此在設計項目間的分層劃分時不只要體現出層級職責,還要考慮到依賴順序。代碼存放在哪一個項目要有講究,不容許出現互相引用的狀況,好比A項目中的代碼引用B,B項目中的代碼又引用A。
應用程序域把資源給隔離開,這個資源,主要指內存。那麼什麼是內存呢?
要知道,程序運行的過程就是電腦不斷經過CPU進行計算的過程,這個過程須要讀取併產生運算的數據,爲此咱們須要一個擁有足夠容量可以快速與CPU交互的存儲容器,這就是內存了。對於內存大小,32位處理器,尋址空間最大爲2的32次方byte,也就是4G內存,除去操做系統所佔用的公有部分,進程大概能佔用2G內存,而若是是64位處理器,則是8T。
而在.NET中,內存區域分爲堆棧和託管堆。
堆和堆棧就內存而言只不過是地址範圍的區別。不過堆棧的數據結構和其存儲定義讓其在時間和空間上都緊密的存儲,這樣能帶來更高的內存密度,能在CPU緩存和分頁系統表現的更好。故而訪問堆棧的速度整體來講比訪問堆要快點。
操做系統會爲每條線程分配必定的空間,Windwos爲1M,這稱之爲線程堆棧。在CLR中的棧主要用來執行線程方法時,保存臨時的局部變量和函數所需的參數及返回的值等,在棧上的成員不受GC管理器的控制,它們由操做系統負責分配,當線程走出方法後,該棧上成員採用後進先出的順序由操做系統負責釋放,執行效率高。
而託管堆則沒有固定容量限制,它取決於操做系統容許進程分配的內存大小和程序自己對內存的使用狀況,託管堆主要用來存放對象實例,不須要咱們人工去分配和釋放,其由GC管理器託管。
不一樣的類型擁有不一樣的編譯時規則和運行時內存分配行爲,咱們應知道,C# 是一種強類型語言,每一個變量和常量都有一個類型,在.NET中,每種類型又被定義爲值類型或引用類型。
使用 struct、enum 關鍵字直接派生於System.ValueType定義的類型是值類型,使用 class、interface、delagate 關鍵字派生於System.Object定義的類型是引用類型。
對於在一個方法中產生的值類型成員,將其值分配在棧中。這樣作的緣由是由於值類型的值其佔用固定內存的大小。
C#中int關鍵字對應BCL中的Int32,short對應Int16。Int32爲2的32位,若是把32個二進制數排列開來,咱們要求既能表達正數也能表達負數,因此得須要其中1位來表達正負,首位是0則爲+,首位是1則爲-,那麼咱們能表示數據的數就只有31位了,而0是介於-1和1之間的整數,因此對應的Int32能表現的就是2的31次方到2的31次方-1,即2147483647和-2147483648這個整數段。
1個字節=8位,32位就是4個字節,像這種以Int32爲表明的值類型,自己就是固定的內存佔用大小,因此將值類型放在內存連續分配的棧中。
而引用類型相比值類型就有點特殊,newobj建立一個引用類型,因其類型內的引用對象能夠指向任何類型,故而沒法準確得知其固定大小,因此像對於引用類型這種沒法預知的容易產生內存碎片的動態內存,咱們把它放到託管堆中存儲。
託管堆由GC託管,其分配的核心在於堆中維護着一個nextObjPtr指針,咱們每次實例(new)一個對象的時候,CLR將對象存入堆中,並在棧中存放該對象的起始地址,而後該指針都會根據該對象的大小來計算下一個對象的起始地址。不一樣於值類型直接在棧中存放值,引用類型則還須要在棧中存放一個表明(指向)堆中對象的值(地址)。
而託管堆又能夠因存儲規則的不一樣將其分類,託管堆能夠被分爲3類:
加載程序集就是將程序集中的信息給映射在加載堆,對產生的實例對象存放至垃圾回收堆。前文說過應用程序域是指經過CLR管理而創建起的邏輯上的內存邊界,那麼每一個域都有其本身的加載堆,只有卸載應用程序域的時候,纔會回收該域對應的加載堆。
而加載堆中的高頻堆包含的有一個很是重要的數據結構表---方法表,每一個類型都僅有一份方法表(MethodTables),它是對象的第一個實例建立前的類加載活動的結果,它主要包含了咱們所關注的3部分信息:
那麼,實例一個對象,CLR是如何將該對象所對應的類型行爲及信息的內存位置(加載堆)關聯起來的呢?
原來,在託管堆上的每一個對象都有2個額外的供於CLR使用的成員,咱們是訪問不到的,其中一個就是類型對象指針,它指向位於加載堆中的方法表從而讓類型的狀態和行爲關聯了起來, 類型指針的這部分概念咱們能夠想象成obj.GetType()方法得到的運行時對象類型的實例。而另外一個成員就是同步塊索引,其主要用於2點:1.關聯內置SyncBlock數組的項從而完成互斥鎖等目的。 2.是對象Hash值計算的輸入參數之一。
能夠看到對於方法中申明的值類型變量,其在棧中做爲一塊值表示,咱們能夠直接經過c#運算符sizeof來得到值類型所佔byte大小。而方法中申明的引用類型變量,其在託管堆中存放着對象實例(對象實例至少會包含上述兩個固定成員以及實例數據,可能),在棧中存放着指向該實例的地址。
當我new一個引用對象的時候,會先分配同步塊索引(也叫對象頭字節),而後是類型指針,最後是類型實例數據(靜態字段的指針存在於方法表中)。會先分配對象的字段成員,而後分配對象父類的字段成員,接着再執行父類的構造函數,最後纔是本對象的構造函數。這個多態的過程,對於CLR來講就是一系列指令的集合,因此不能糾結new一個子類對象是否會也會new一個父類對象這樣的問題。而也正是由於引用類型的這樣一個特徵,咱們雖然能夠估計一個實例大概佔用多少內存,但對於具體佔用的大小,咱們須要專門的工具來測量。
對於引用類型,u2=u1,咱們在賦值的時候,實際上賦的是地址,那麼我改動數據其實是改動該地址指向的數據,這樣一來,由於u2和u1都指向同一塊區域,因此我對u1的改動會影響到u2,對u2的改動會影響到u1。若是我想互不影響,那麼我能夠繼承IClone接口來實現內存克隆,已有的CLR實現是淺克隆方法,但也只能克隆值類型和String(string是個特殊的引用類型,對於string的更改,其會產生一個新實例對象),若是對包含其它引用類型的這部分,咱們能夠本身經過其它手段實現深克隆,如序列化、反射等方式來完成。而若是引用類型中包含有值類型字段,那麼該字段仍然分配在堆上。
對於值類型,a=b,咱們在賦值的時候,其實是新建了個值,那麼我改動a的值那就只會改動a的值,改動b的值就只會改動b的值。而若是值類型(如struct)中包含的有引用類型,那麼還是一樣的規則,引用類型的那部分實例在託管堆中,地址在棧上。
我若是將值類型放到引用類型中(如:object a=3),會在棧中生成一個地址,在堆中生成該值類型的值對象,還會再生成這類型指針和同步塊索引兩個字段,這也就是常說裝箱,反過來就是拆箱。每一次的這樣的操做,都會涉及到內存的分佈、拷貝,可見,裝箱和拆箱是有性能損耗,所以應該減小值類型和引用類型之間轉換的次數。
但對於引用類型間的子類父類的轉換,僅是指令的執行消耗,幾盡沒有開銷。
值得注意的是,當我new完一個對象再也不使用的時候,這個對象在堆中所佔用的內存如何處理?
在非託管世界中,能夠經過代碼手動進行釋放,但在.NET中,堆徹底由CLR託管,也就是說GC堆是如何具體來釋放的呢?
當GC堆須要進行清理的時候,GC收集器就會經過必定的算法來清理堆中的對象,而且版本不一樣算法也不一樣。最主要的則爲Mark-Compact標記-壓縮算法。
這個算法的大概含義就是,經過一個圖的數據結構來收集對象的根,這個根就是引用地址,能夠理解爲指向託管堆的這根關係線。當觸發這個算法時,會檢查圖中的每一個根是否可達,若是可達就對其標記,而後在堆上找到剩餘沒有標記(也就是不可達)的對象進行刪除,這樣,那些不在使用的堆中對象就刪除了。
前面說了,由於nextObjPtr的緣故,在堆中分配的對象都是連續分配的,由於未被標記而被刪除,那麼通過刪除後的堆就會顯得支零破碎,那麼爲了不空間碎片化,因此須要一個操做來讓堆中的對象再變得緊湊、連續,而這樣一個操做就叫作:Compact壓縮。
而對堆中的分散的對象進行挪動後,還會修改這些被挪動對象的指向地址,從而得以正確的訪問,最後從新更新一下nextObjPtr指針,周而復始。
而爲了優化內存結構,減小在圖中搜索的成本,GC機制又爲每一個託管堆對象定義了一個屬性,將每一個對象分紅了3個等級,這個屬性就叫作:代,0代、1代、2代。
每當new一個對象的時候,該對象都會被定義爲第0代,當GC開始回收的時候,先從0代回收,在這一次回收動做以後,0代中沒有被回收的對象則會被定義成第1代。當回收第1代的時候,第1代中沒有被清理掉的對象就會被定義到第2代。
CLR初始化時會爲0/1/2這三代選擇一個預算的容量。0代一般以256 KB-4 MB之間的預算開始,1代的典型起始預算爲512 KB-4 MB,2代不受限制,最大可擴展至操做系統進程的整個內存空間。
好比第0代爲256K,第1代爲2MB。咱們不停的new對象,直到這些對象達到256k的時候,GC會進行一次垃圾回收,假設此次回收中回收了156k的不可達對象,剩餘100k的對象沒有被回收,那麼這100k的對象就被定義爲第1代。如今就變成了第0代裏面什麼都沒有,第1代裏放的有100k的對象。這樣周而復始,GC清除的永遠都只有第0代對象,除非當第一代中的對象累積達到了定義的2MB的時候,纔會連同清理第1代,而後第1代中活着的部分再升級成第二代...
第二代的容量是沒有限制,可是它有動態的閾值(由於等到整個內存空間已滿以執行垃圾回收是沒有意義的),當達到第二代的閾值後會觸發一次0/1/2代完整的垃圾收集。
也就是說,代數越長說明這個對象經歷了回收的次數也就越多,那麼也就意味着該對象是不容易被清除的。
這種分代的思想來將對象分割成新老對象,進而配對不一樣的清除條件,這種巧妙的思想避免了直接清理整個堆的尷尬。
那麼除了經過new對象而達到代的閾(臨界)值時,還有什麼可以致使垃圾堆進行垃圾回收呢? 還可能windows報告內存不足、CLR卸載AppDomain、CLR關閉等其它特殊狀況。
或者,咱們還能夠本身經過代碼調用。
.NET有GC來幫助開發人員管理內存,而且版本也在不斷迭代。GC幫咱們託管內存,但仍然提供了System.GC類讓開發人員可以輕微的協助管理。 這其中有一個能夠清理內存的方法(並無提供清理某個對象的方法):GC.Collect方法,能夠對全部或指定代進行即時垃圾回收(若是想調試,需在release模式下才有效果)。這個方法儘可能別用,由於它會擾亂代與代間的秩序,從而讓低代的垃圾對象跑到生命週期長的高代中。
GC還提供了,判斷當前對象所處代數、判斷指定代數經歷了多少次垃圾回收、獲取已在託管堆中分配的字節數這樣的三個方法,咱們能夠從這3個方法簡單的瞭解託管堆的狀況。
託管世界的內存不須要咱們打理,咱們沒法從代碼中得知具體的託管對象的大小,你若是想追求對內存最細微的控制,顯然C#並不適合你,不過相似於有關內存把控的這部分功能模塊,咱們能夠經過非託管語言來編寫,而後經過.NET平臺的P/Invoke或COM技術(微軟爲CLR定義了COM接口並在註冊表中註冊)來調用。
像FCL中的源碼,不少涉及到操做系統的諸如 文件句柄、網絡鏈接等外部extren的底層方法都是非託管語言編寫的,對於這些非託管模塊所佔用的資源,咱們能夠經過隱式調用析構函數(Finalize)或者顯式調用的Dispose方法經過在方法內部寫上非託管提供的釋放方法來進行釋放。
像文中示例的socket就將釋放資源的方法寫入Dispose中,析構函數和Close方法均調用Dispose方法以此完成釋放。事實上,在FCL中的使用了非託管資源的類大多都遵循IDispose模式。而若是你沒有釋放非託管資源直接退出程序,那麼操做系統會幫你釋放該程序所佔的內存的。
還有一點,垃圾回收是對性能有影響的。
GC雖然有不少優化策略,但總之,只要當它開始回收垃圾的時候,爲了防止線程在CLR檢查期間對對象更改狀態,因此CLR會暫停進程中的幾乎全部線程(因此線程太多也會影響GC時間),而暫停的時間就是應用程序卡死的時間,爲此,對於具體的處理細節,GC提供了2種配置模式讓咱們選擇。
第一種爲:單CPU的工做站模式,專爲單CPU處理器定作。這種模式會採用一系列策略來儘量減小GC回收中的暫停時間。
而工做站模式又分爲併發(或後臺)與不併發兩種,併發模式表現爲響應時間快速,不併發模式表現爲高吞吐量。
第二種爲:多CPU的服務器模式,它會爲每一個CPU都運行一個GC回收線程,經過並行算法來使線程能真正同時工做,從而得到性能的提高。
咱們能夠經過在Config文件中更改配置來修改GC模式,若是沒有進行配置,那麼應用程序老是默認爲單CPU的工做站的併發模式,而且若是機器爲單CPU的話,那麼配置服務器模式則無效。
若是在工做站模式中想禁用併發模式,則應該在config中運行時節點添加 <gcConcurrent enabled="false" />
若是想更改至服務器模式,則能夠添加 <gcServer enabled="true" />。
<configuration> <runtime> <!--<gcConcurrent enabled="true|false"/>--> <!--<gcServer enabled="true|false"/>--> </runtime> </configuration>
至此,.NET Framework上的三個重要概念,程序集、應用程序域、內存在本文講的差很少了,我畫了一張圖簡單的概述.NET程序的一個執行流程:
我在前文對.NET系統概述時,有的直接稱.NET,有的稱.NET Framework。那麼準確來講什麼是.NET?什麼又是.NET Framework呢?
.NET是一個微軟搭造的開發者平臺,它主要包括:
事實上,像我上面講的那些諸如程序集、GC、AppDomain這樣的爲CLR的一些概念組成,實質上指的是.NET Framework CLR。
.NET平臺是微軟爲了佔據開發市場而成立的,不是無利益驅動的純技術平臺的那種東西。基於該平臺下的技術框架也由於 商業間的利益 從而和微軟自身的Windows操做系統所綁定。因此雖然平臺雄心和口號很大,但不少框架類庫技術都是以Windows系統爲藍本,這樣就致使,雖然.NET各方面都挺好,可是用.NET就必須用微軟的東西,直接造成了技術-商業的綁定。
.NET Framework就是.NET 技術框架組成在Windows系統下的具體的實現,和Windows系統高度耦合,上文介紹的.NET系統,就是指.NET Framework。
有醜纔有美,有低纔有高,概念是比較中誕生的。.NET Core就是如此,它是其它操做系統的.NET Framework翻版實現。
操做系統不止Windows,還有Mac和類Linux等系統, .NET的實現 若是按操做系統來橫向分割的話,能夠分爲 Windows系統下的 .NET Framework 和 兼容多個操做系統的 .NET Core。
咱們知道,一個.NET程序運行核心在於.NET CLR,爲了能讓.NET程序在其它平臺上運行,一些非官方社區和組織爲此開發了在其它平臺下的.NET實現(最爲表明的是mono,其團隊後來又被微軟給合併了 ),但由於不是官方,因此在一些方面多少有些缺陷(如FCL),後來微軟官方推出了.NET Core,其開源在Github中,並被收錄在NET基金會(.NET Foundation,由微軟公司成立與贊助的獨立自由軟件組織,其目前收錄包括.NET編譯器平臺("Roslyn")以及ASP.NET項目系列,.NET Core,Xamarin Forms以及其它流行的.NET開源框架),旨在真正的 .NET跨平臺。
.NET Core是.NET 技術框架組成在Windows.macOS.Linux系統下的具體的實現。
.NET Core是一個開源的項目,其由 Microsoft 和 GitHub 上的 .NET 社區共同維護,但 這份工做仍然是巨大的,由於在早期對.NET上的定義及最初的實現一直是以Windows系統爲參照及載體,一些.NET機制實際上與Windows系統耦合度很是高,有些屬於.NET本身體系內的概念,有些則屬於Windows系統api的封裝。 那麼從Windows轉到其它平臺上,不只要實現相應的CLR,還要捨棄或重寫一部分BCL,於是,.NET Core在概念和在項目中的行爲與咱們日常有些不一樣。
好比,NET Core不支持AppDomains、遠程處理、代碼訪問安全性 (CAS) 和安全透明度,任何有關該概念的庫代碼都應該被替換。 這部分代碼它不只指你項目中的代碼,還指你項目中using的那些程序集代碼,因此你會在github上看到不少開源項目都在跟進對.NET Core的支持,而且不少開發者也嘗試學習.NET Core,這也是一種趨勢。