記憶體回收的基本概念

在通用語言執行平台 (CLR) 中,記憶體回收行程 (GC) 會當做自動記憶體管理員。 記憶體回收行程可管理應用程式的記憶體配置及釋放。 因此,使用受控程式碼的開發人員不須撰寫程式碼以執行記憶體管理工作。 自動記憶體管理可排除一些常見的問題,例如,忘記釋放物件而造成記憶體流失,或嘗試存取已釋放物件的已釋放記憶體。

本文描述記憶體回收的核心概念。

優點

記憶體回收行程提供下列優點:

  • 開發人員無須手動釋放記憶體。

  • 有效率地在 Managed 堆積上配置物件。

  • 回收不再使用的物件、清除其記憶體,並且讓記憶體可供未來的配置使用。 受控物件在開始時會自動取得乾淨的內容,因此其建構函式不需要初始化每個資料欄位。

  • 透過確定物件無法用於配置給其他物件的記憶體,提供記憶體安全性。

記憶體的基本概念

下列清單摘要說明重要的 CLR 記憶體概念:

  • 每個處理序都有各自獨立的虛擬位址空間。 同一部電腦上的所有處理序會共用相同的實體記憶體和分頁檔 (如果有的話)。

  • 根據預設,在 32 位元電腦上,每個處理序都有 2 GB 使用者模式虛擬位址空間。

  • 身為應用程式開發人員,您只會處理虛擬位址空間,絕不會直接操作實體記憶體。 記憶體回收行程會在 Managed 堆積上自動配置和釋出虛擬記憶體。

    如果您要撰寫原生程式碼,可使用 Windows 函式來處理虛擬位址空間。 這些函式會在原生堆積上自動配置和釋出虛擬記憶體。

  • 虛擬記憶體可以有三種狀態:

    State 描述
    免費 記憶體區塊沒有任何參考,可進行配置。
    保留 記憶體區塊可供您使用,但是無法用於任何其他配置要求。 不過,在此記憶體區塊認可之前,您無法將資料儲存到其中。
    已認可 記憶體區塊會指派給實體儲存區。
  • 虛擬位址空間可分散,亦即位址空間中稱為空白位置的可用區塊。 要求虛擬記憶體配置時,虛擬記憶體管理程式必須找到大小可滿足配置要求的單一可用區塊。 即使您擁有 2GB 可用空間,要求 2GB 的配置仍然不會成功,除非該可用空間全都在單一位址區塊中。

  • 如果沒有足夠保留用的虛擬位址空間或認可用的實體空間,則可能會用盡記憶體。

    即使實體記憶體壓力 (實體記憶體的需求) 不高,仍會使用分頁檔。 實體記憶體第一次面臨壓力高的情況時,作業系統必須釋出實體記憶體的空間來儲存資料,而且它會將實體記憶體中的部分資料備份到分頁檔。 資料只會在需要時進行分頁,因此可能在實體記憶體壓力低的情況下發生分頁。

記憶體配置

當您初始化新的處理序 (Process) 時,Runtime 會保留一塊連續的位址空間區域,供處理序使用。 這塊保留的位址空間稱為 Managed 堆積 (Heap)。 Managed 堆積會保留即將配置給堆積中下一個物件的位址指標。 剛開始會將這個指標設定為 Managed 堆積的基底位址 (Base Address)。 所有參考類型都是在 Managed 堆積上進行配置。 當應用程式建立第一個參考型別時,會為該型別配置 Managed 堆積基底位址的記憶體。 當應用程式建立第二個物件時,執行階段會為其配置緊接在第一個物件後面位址空間的記憶體。 只要有可供使用的位址空間,執行階段就會繼續用這種方式為新的物件配置空間。

從 Managed 堆積中配置記憶體要比 Unmanaged 記憶體配置快。 由於執行階段是用增加指標值的方式為物件配置記憶體,因此速度幾乎和從堆積中配置記憶體一樣快。 此外,由於連續配置的新物件是連續儲存在受控堆積中,因此應用程式可以快速存取這些物件。

記憶體釋放

記憶體回收行程的最佳化引擎會根據所做的配置,決定執行回收的最佳時機。 當記憶體回收行程執行回收時,會將應用程式已經不再使用的物件記憶體釋放出來。 其會檢查應用程式的根目錄,判斷哪些物件已不再使用。 應用程式的根目錄包含靜態欄位、執行緒堆積上的區域變數、CPU 暫存器、GC 控制代碼和完成佇列。 每一個根目錄都會參考 Managed 堆積上的物件,要不然就是設定為 Null。 記憶體回收行程可要求這些根目錄的其餘執行階段。 記憶體回收行程會使用此清單建立圖表,其中包含可從根目錄取得的所有物件。

不在圖形中的物件就無法從應用程式的根目錄取得。 記憶體回收行程會考慮無法取得的物件記憶體,然後將配置給其記憶體釋放出來。 在回收期間,記憶體回收行程會檢查 Managed 堆積,尋找無法取得的物件所佔用的位址空間區塊。 每找到一個無法取得的物件時,它便會使用記憶體複製功能,壓縮記憶體中可取得的物件,然後釋放出為無法取得的物件所配置的位址空間區塊。 一旦壓縮完可取得物件的記憶體之後,記憶體回收行程會進行必要的指標更正,使應用程式的根目錄指向新位置中的物件。 它也會將 Managed 堆積的指標放在最後取得物件的後面。

只有當回收找到相當數量無法取得的物件時,才會壓縮記憶體。 如果受控堆積中的所有物件在回收之後都存留下來,就不需要壓縮記憶體。

為了改善效能,Runtime 會為大型物件配置不同堆積中的記憶體。 記憶體回收行程會自動釋放大型物件的記憶體。 但是,為了避免移動記憶體中的大型物件,因此通常不會壓縮記憶體。

記憶體回收的條件

當下列其中一個條件成立時,就會進行記憶體回收:

  • 系統的實體記憶體不足。 記憶體大小是透過來自作業系統的低記憶體通知或由主機指出的低記憶體來偵測。

  • 由受控堆積上已配置物件所使用的記憶體超過可接受的閾值。 這個臨界值會在處理序執行時持續調整。

  • 即會呼叫 GC.Collect 方法。 在大多數的情況下,您不需要呼叫這個方法,因為記憶體回收行程會持續執行。 這個方法主要用於獨特的情況和測試。

Managed 堆積

CLR 初始化記憶體回收行程之後,記憶體回收行程就會配置用來儲存和管理物件的記憶體區段。 這個記憶體稱為 Managed 堆積,與作業系統中的原生堆積相反。

每個受控處理序都有一個受控堆積。 處理序中的所有執行緒都會對相同堆積上的物件配置記憶體。

為節省記憶體,記憶體回收行程會呼叫 Windows VirtualAlloc 函式,並且針對受控應用程式一次保留一個記憶體區段。 記憶體回收行程也會視需要保留區段,並且透過呼叫 Windows VirtualFree 函式,將區段釋放回作業系統 (在清除任何物件的區段之後)。

重要

記憶體回收行程所配置的區段大小是依實作而定,有可能在任何時間,包括在定期更新時做變更。 您的應用程式永遠都不應該對相關或根據特定區段的大小做出假設,也不應嘗試設定區段配置的可用記憶體數量。

配置在堆積上的物件越少,記憶體回收行程必須進行的工作就越少。 當您配置物件時,請勿使用超出需求的進位值,例如,當您只需要 15 個位元組時,卻配置 32 個位元組的陣列。

觸發記憶體回收時,記憶體回收行程就會回收無作用物件所佔據的記憶體。 回收處理序會壓縮使用中物件,讓其集合在一起,並且移除無作用物件,因而讓堆積更小。 此處理序可確保一起配置的物件會在受控堆積上集中,以便保持其區域性。

記憶體回收的干擾程度 (頻率和持續期間) 是 Managed 堆積上之配置量和未被回收記憶體數量的結果。

您可以將此堆積視為兩個堆積的累積:大型物件堆積和小型物件堆積。 大型的物件堆積包含 85,000 位元組以上 (通常是陣列) 的物件。 超大型的執行個體物件非常罕見。

提示

您可設定閾值大小,讓物件移至大型物件堆積上。

層代

GC 演算法取決於多項考量項目:

  • 壓縮部分受控堆積的記憶體比壓縮整個受控堆積要快。
  • 較新物件的存留期較短,較舊物件的存留期較長。
  • 較新的物件通常都會彼此相關,而且應用程式也會同時存取。

記憶體回收主要發生在回收短期物件時。 為了最佳化記憶體回收行程的效能,受控堆積分成三個層代 0、1 和 2,以便可分別處理長期和短期物件。 記憶體回收行程會將新的物件儲存在層代 0。 在應用程式存留期初期建立的物件則會升階並儲存在第 1 個和第 2 個層代。 由於壓縮部分受控堆積比壓縮整個堆積要快,因此這種配置允許記憶體回收行程釋放指定層代的記憶體,而不是在每次執行回收時釋放整個受控堆積的記憶體。

  • 層代 0:此為最新的層代而且包含存留較短的物件。 存留較短的物件範例是暫存變數。 記憶體回收最常在這個層代中進行。

    新配置的物件會形成新的物件層代且隱含為層代 0 集合。 不過,如果這些物件為大型物件,則會移至大型物件堆積 (LOH) 上,這有時稱為層代 3。 層代 3 為實體層代,邏輯上會回收作為層代 2 的一部分。

    大部分物件都會在層代 0 的記憶體回收中回收,而且不會存留至下一個層代。

    當層代 0 已滿時,如果應用程式嘗試建立新的物件,記憶體回收行程會執行回收以釋放物件的位址空間。 記憶體回收行程是從檢查第 0 個層代中的物件開始,而不是檢查 Managed 堆積中的所有物件。 單獨回收層代 0 通常就能收回足夠的記憶體,讓應用程式繼續建立新的物件。

  • 層代 1:這個層代包含存留較短的物件,而且當做存留較短物件與存留較長物件之間的緩衝區。

    記憶體回收行程執行層代 0 的回收之後,將壓縮可取得物件的記憶體並將其升階至層代 1。 由於回收之後存留下來的物件通常具有較長的存留期,因此才會將它們提升至較高的層代。 每當記憶體回收行程執行層代 0 的回收時,就不需要再重新檢查層代 1 和 2 中的物件了。

    如果層代 0 的回收沒有收回足夠的記憶體讓應用程式建立新的物件,記憶體回收行程可以執行層代 1 的回收,然後再執行層代 2 的回收。 在回收之後存留下來的第 1 個層代物件則會提升至第 2 個層代。

  • 層代 2:這個層代包含存留較長的物件。 存留較長的物件範例是伺服器應用程式中包含處理序持續期間存留之靜態資料的物件。

    回收之後存留下來的層代 2 物件仍然會留在層代 2 中,直到未來回收時判斷這些物件無法取得為止。

    大型物件堆積上的物件 (有時候稱為層代 3) 也會在層代 2 中回收。

當條件許可時,記憶體回收會針對特定層代進行。 回收層代是指回收該層代中的物件及其所有較新的層代。 層代 2 記憶體回收也稱為完整記憶體回收,因為其會回收所有層代中的物件 (亦即,Managed 堆積中的所有物件)。

未回收和提升

沒有在記憶體回收中回收的物件稱為未回收物件,而且會提升至下一個層代:

  • 層代 0 記憶體回收之後存留下來的物件會升階至層代 1。
  • 層代 1 記憶體回收之後存留下來的物件會升階至層代 2。
  • 層代 2 記憶體回收之後存留下來的物件會維持在層代 2。

當記憶體回收行程偵測出某個層代的未回收率很高時,就會增加該層代的配置閾值。 下一個回收會取得大量的回收記憶體。 CLR 會持續在兩個優先權之間取得平衡:透過刪除記憶體回收,不讓應用程式的工作集變得太大,而且不讓記憶體回收執行太頻繁。

暫時層代和區段

因為層代 0 和層代 1 中的物件存留較短,所以這些層代稱為暫時層代。

暫時層代會配置於稱為暫時區段的記憶體區段中。 記憶體回收行程所取得的每個新區段都會成為新的暫時區段,而且包含在層代 0 記憶體回收中未被回收的物件。 舊的暫時區段會成為新的層代 2 區段。

暫時區段的大小會根據系統是 32 位元或 64 位元,以及根據系統所執行 (工作站或伺服器 GC) 的記憶體回收行程類型而有所不同。 下表顯示暫時區段的預設大小:

工作站/伺服器 GC 32 位元 64 位元
工作站 GC 16 MB 256 MB
伺服器 GC 64 MB 4 GB
具有 > 4 個邏輯 CPU 的伺服器 GC 32 MB 2 GB
具有 > 8 個邏輯 CPU 的伺服器 GC 16 MB 1 GB

暫時區段可能會包括層代 2 物件。 層代 2 物件可以使用多個區段,取決於處理序所需而且記憶體允許的數目。

暫時記憶體回收中釋放記憶體的數量會限制為暫時區段的大小。 所釋放的記憶體數量會與無作用物件所佔據的空間成正比。

記憶體回收期間進行的作業

記憶體回收具有下列階段:

  • 標記階段:尋找和建立所有使用中物件的清單。

  • 重新配置階段:更新即將壓縮之物件的參考。

  • 壓縮階段:回收無作用物件所佔據的空間並壓縮未被回收的物件。 壓縮階段會將記憶體回收中未被回收的物件移至區段的較舊端。

    因為層代 2 回收可能會佔據多個區段,所以提升至層代 2 的物件可能會移至較舊區段。 層代 1 和層代 2 的未回收物件都可能會移至不同的區段,因為都會被升階至層代 2。

    正常情況下,大型物件堆積 (LOH) 不會壓縮,因為複製大型物件會帶來效能損失。 不過,在 .NET Core 和 .NET Framework 4.5.1 和更新版本中,您可以使用 GCSettings.LargeObjectHeapCompactionMode 屬性來視需要壓縮大型物件堆積。 此外,當透過指定下列其中一項來設定硬性限制時,便會自壓縮 LOH:

記憶體回收行程會使用下列資訊來判斷物件是否使用中:

  • 堆疊根目錄:Just-in-Time (JIT) 編譯器和堆疊查核器所提供的堆疊變數。 JIT 最佳化可以延長或縮短程式碼區域,其中堆疊變數會向記憶體回收行程回報。

  • 記憶體回收控制代碼:會指向受控物件,而且可由使用者程式碼或通用語言執行平台配置的控制代碼。

  • 靜態資料:應用程式定義域中可能參考其他物件的靜態物件。 每個應用程式定義域都會追蹤其靜態物件。

記憶體回收開始之前,所有 Managed 執行緒都會暫停,但觸發記憶體回收的執行緒除外。

下圖顯示觸發記憶體回收且造成其他執行緒暫停的執行緒:

執行緒如何觸發記憶體回收的螢幕擷取畫面。

非受控資源

對於應用程式所建立的大部分物件而言,您都可以依賴記憶體回收自動執行必要的記憶體管理工作。 但是,Unmanaged 資源需要明確清除。 最常見的 Unmanaged 資源類型就是包裝作業系統資源 (例如檔案控制代碼、視窗控制代碼或網路連接) 的物件。 雖然記憶體回收行程可追蹤封裝非受控資源的受控物件存留期,但是它並沒有關於如何清除資源的相關資訊。

當定義封裝非受控資源的物件時,建議您提供必要的程式碼,在公用 Dispose 方法中清除受控資源。 您可以提供 Dispose 方法,讓物件的使用者在用完物件後,明確釋放資源。 當您使用封裝非受控資源的物件時,應確定在必要時呼叫 Dispose

您必須也提供方式以在您的類型消費者忘記呼叫 Dispose 時釋放非受控資源。 您可使用安全控制代碼來包裝非受控資源或覆寫 Object.Finalize() 方法。

如需清除非受控資源的詳細資訊,請參閱清除非受控資源

另請參閱