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



參考資料:

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

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

托管代碼

在 .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)。

自動內存管理

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

垃圾回收

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、1、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、1、2 代堆;

微信圖片_20210110194803

今天就水到這里為止。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM