淺入 .NET Core 中的內存和GC知識


參考資料:算法

【1】https://docs.microsoft.com/zh-cn/dotnet/standard/managed-codewindows

【2】:https://docs.microsoft.com/zh-cn/dotnet/standard/clrapi

託管代碼

在 .NET 中, CLR(Common Language Runtime) 負責提取託管代碼並編譯成機器語言,而後執行它。在此過程當中,CLR 提供自動內存管理、安全邊界、類型安全等服務,保證了代碼安全。安全

託管代碼指在其執行過程當中由 CLR(Common Language Runtime) 管理的代碼,託管代碼是可在 .NET 上運行得一種高級語言(C#、F#等),編寫的託管代碼被編譯後會被生成 中間語言(IL)。微信

CLR 有 .NET Core/.NET5+、Mono、.NET Framework 等實現,託管代碼生成的文件(IL代碼)不能被操做系統直接運行,須要 CLR 的實現(如 .NET5) 託管運行,託管過程當中對其再次編譯生成二進制代碼(JIT編譯)。函數

中間語言(IL)有時也稱爲公共中間語言 (CIL) 或 Microsoft 中間語言 (MSIL)。this

自動內存管理

自動內存管理是 CLR 的功能之一,它能夠爲應用程序管理內存的分配和釋放,託管代碼被執行時,由 CLR 進行內存管理,保證了內存安全。spa

垃圾回收

GC

GC(garbage collector)中文譯爲垃圾回收器,.NET 中的 GC 指的是 CLR 中的自動內存管理器,GC 負責管理 .NET 程序的內存分配和釋放操作系統

GC 的優勢以下:設計

  • 自動管理內存,沒必要手動分配和釋放;

  • 高效管理託管堆上的對象;

  • 智能回收對象,清除內存;

  • 內存安全:避免野指針、懸空指針等狀況形成嚴重錯誤;

內存

物理內存

物理內存是物理內存條上的內存空間,是物理機器真實的容量大小。

虛擬內存

虛擬內存(Virtual Memory)是計算機操做系統進行內存管理的一種技術,它能夠將多個硬件、非連續地址的碎片空間組合起來,造成進程上可識別的連續內存空間。

虛擬內存由操做系統進行支持,如 Windows 上的虛擬內存,Linux 上的交互空間,虛擬內存須要操做系統映射到真實的內存地址空間才能使用。虛擬內存調度方式有分頁式、段式、段頁式3種,讀者感興趣可自行查閱資料。

現代操做系統都採用了虛擬內存管理技術,經過對物理存儲設備的抽象,操做系統調度外存看成內存使用,提供了比物理內存更大的內存範圍。

這些存儲設備組成的內存稱爲虛擬地址空間,而用戶(開發者)接觸到的地址是虛地址,並非真實的物理地址。虛擬空間大大拓展了內存,使得系統能夠同時運行多道程序而不「吃力」。

虛擬地址空間分爲兩部分:用戶空間、內核空間,每一個程序運行時的會消耗兩種空間。在 Linux 中比例是 3:1,在 Windows 中是 2:2。

.NET 內存組成

.NET 中,內存分爲非託管內存、託管內存。

.NET Core/.NET5+ 有一個稱爲 dotnet 的驅動程序,此驅動程序用於執行命令或運行 .NET 程序。當咱們使用 dotnet 命令運行一個 .dll 文件時,操做系統會啓動 dotnet 驅動程序,此時會分配操做系統內存資源、dotnet 驅動程序內存資源,這一部分即非託管資源,其中 dotnet 部分的內存包含了 CLR 等部件的內存。即便你並無使用到 C/C++ 等非託管代碼或者使用非託管資源,也會使用到非託管內存。

接下來 CLR 將初始化新進程,CLR 將爲其分配託管內存(託管堆),這段託管內存是一個連續的地址空間區域。.NET 安全代碼只能使用託管內存,不能直接使用物理內存,垃圾收集器會爲安全代碼在託管堆上分配和釋放虛擬內存。

顯然, dotnet 的工做原理十分複雜,筆者沒有能力講清楚,感興趣的讀者能夠自行查閱資料。

CLR 中的內存

微軟 .NET CLR 文檔中寫道:By default, on 32-bit computers, each process has a 2-GB user-mode virtual address space.

即在 32 位系統中,.NET 進程會使用 2GB 的用戶模式虛擬內存,其虛擬地址空間的表示範圍是 0x00000000 到 0x7fff;而 64 位系統中,地址範圍是 0x000'00000000 到0x7FFF'FFFFFFFF,約等於 16TB。

從以上信息,咱們知道 .NET 程序會消耗比較多的虛擬內存,若是在 64 位操做系統上運行 .NET 程序,其用戶模式虛擬地址空間可能遠遠大於 2GB。

編寫一個 "c1" 程序,其代碼以下:

static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");
            Console.Read();
        }

在 Linux 中使用 dotnet xx.dll 命令運行程序,而後查看其佔用的資源:

VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
  3.1g   0.0g   0.0g S   0.3   0.3   0:00.83 dotnet

使用 dotnet-counters 查看 dotnet 進程:

GC Heap Size (MB)                                              0
    Gen 0 GC Count (Count / 1 sec)                                 0
    Gen 0 Size (B)                                                 0
    Gen 1 GC Count (Count / 1 sec)                                 0
    Gen 1 Size (B)                                                 0
    Gen 2 GC Count (Count / 1 sec)                                 0
    Gen 2 Size (B)                                                 0
    LOH Size (B)                                                   0

注:使用 dotnet run 運行 .NET 項目,會出現 dotnet、c1 兩個進程,能夠看到會產生 dotnet 和 c1 兩個進程,dotnet 是驅動程序,dotnet 啓動後,CLR 會將. dll 程序集編譯,並初始化啓動一個進程。

CLR 中的虛擬地址空間須要位於一個地址塊中,由於在請求虛擬內存分配時,虛擬內存管理器必須找到知足需求的單個可用塊,例如就算存在大於 2GB 的虛擬地址空間,但若是不是連續的,則會分配失敗。若是沒有足夠的可供保留的虛擬地址空間或可供提交的物理空間,則可能會用盡內存。

CLR 虛擬內存狀態

CLR 中的虛擬內存能夠有三種狀態:

State Description
Free 可用 The block of memory has no references to it and is available for allocation. 內存塊沒有對它的引用,能夠進行分配
Reserved保留 The block of memory is available for your use and cannot be used for any other allocation request. 該內存塊可供您使用,不能用於任何其餘分配請求 However, you cannot store data to this memory block until it is committed. 可是,在提交數據以前,不能將數據存儲到此內存塊中
Committed已提交 The block of memory is assigned to physical storage. 內存塊已指派給物理存儲

內存分配

CLR 在初始化新進程時,會爲進程保留一個連續的地址空間區域,這個地址空間被稱爲託管堆。託管堆中維護着一個指針,最初此指針指向託管堆的基址,這個指針是向後移動的。當須要分配內存時,CLR 便會分配位於此指針後的內存區域,同時指針指向此對象地址空間以後的位置。

內存分配

因爲 CLR 經過向指針添加值來爲對象分配內存,因此它的分配速度幾乎跟從堆棧中分配內存速度同樣快;並且連續分配的新對象連續存儲在託管堆中,程序能夠快速地訪問這些對象。

當 GC 回收內存時,一些對象釋放後內存會被回收,這樣託管堆地內存處於碎片化,以後整個內存段會被壓縮,從新組成連連續的內存段,指針會被重置到對象的末尾。

固然,大對象堆(LOH)回收並不會壓縮內存段,這一點咱們後面再討論。

內存釋放

垃圾回收的條件

根據微軟官方文檔,整理的垃圾回收條件以下:

  • 系統物理內存不足;
  • 託管堆分配的內存已超出可接受閾值;(固然,這個閾值會被動態調整)
  • 手動調用 GC 類的 API(例如 GC.Collect);

託管堆

本機堆(Native Heap)

前面提到過,.NET 的內存有非託管內存和託管內存。CLR 運行的進程,存在本機堆和託管堆兩種內存堆,本機內存堆經過 Windows API 的 VirtualAlloc 函數分配,提供給 操做系統和 CLR 使用,用於非託管代碼所需的內存。

託管堆(Managed Heap)

關於託管堆,前面已經寫了,這裏再也不贅述。

託管堆代數

託管堆中的內存被分爲三代,分別使用0、一、2 標識,GC 分配的內存首先在 0 代託管堆中,當進行垃圾回收時,若是對象沒有被釋放,則將其升級並存儲到 1 代託管堆中。1 代託管堆進行內存回收時,不被釋放的對象也會被升級到 2 代內存中,而後 1 代內存堆進行空間壓縮。

託管堆的管理是 GC 負責的,而 GC 進行內存分配和釋放,使用了 GC 算法。

GC 算法基於如下理論:

  • ① 壓縮託管堆的一部份內存要比壓縮整個託管堆速度快;
  • ② 較新的對象生命週期較短,較舊的對象生命週期較長;
  • ③ 較新的對象趨向於相互關聯,而且大約在同一時間被應用程序訪問;

咱們必須深入理解這些理論,才能深刻理解託管堆的設計。

關於 0 到 2 代堆,其基本說明以下:

  • 0 代:0 代中的對象擁有短暫的生命週期,垃圾回收最常發生在此代中;
  • 1 代:做爲生命週期較短和生命週期較長對象的緩衝區。
  • 2 代:存儲生命週期長的對象;0、1 代沒被回收而升級的對象會升級到 2 代中,靜態數據等則會一開始就分配到 2代。

在 .NET 5 以前,.NET 有 SOH(小對象堆)、LOH(大對象堆);在 .NET 5 中,出現了 POH ;

小對象堆的內存段有 0、一、2 代堆;

微信圖片_20210110194803

今天就水到這裏爲止。

相關文章
相關標籤/搜索