記憶體回收的基本概念

在 Common Language Runtime (CLR) 中,垃圾收集行程 (GC) 可作為自動記憶體管理員。 垃圾收集行程會管理應用程式的記憶體配置和釋放。 因此,使用 Managed 程式碼的開發人員不需要撰寫程式碼來執行記憶體管理工作。 自動記憶體管理可以消除常見問題,例如忘記釋放物件,並導致記憶體流失,或嘗試存取已釋放之物件的記憶體。

本文說明垃圾收集的核心概念。

優點

垃圾收集行程提供下列優點:

  • 釋放開發人員不必手動釋放記憶體。

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

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

  • 藉由確保物件無法用於本身為另一個物件配置的記憶體,以提供記憶體安全性。

記憶體的基本概念

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

  • 每個處理序都有各自獨立的虛擬位址空間。 相同電腦上的所有進程都會共用相同的實體記憶體和頁面檔案,如果有的話。

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

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

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

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

    State 描述
    免費 記憶體區塊沒有任何參考,可進行配置。
    保留 記憶體區塊可供您使用,無法用於任何其他配置要求。 不過,在認可資料之前,您無法將資料儲存到此記憶體區塊。
    已認可 記憶體區塊會指派給實體儲存區。
  • 虛擬位址空間可能會分散,這表示位址空間中有稱為漏洞的可用區塊。 要求虛擬記憶體配置時,虛擬記憶體管理員必須尋找足以滿足配置要求的單一可用區塊。 即使您有 2 GB 的可用空間,除非所有可用空間都在單一位址區塊中,否則需要 2 GB 的配置將會失敗。

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

    即使實體記憶體壓力 (實體記憶體) 的需求很低,仍會使用頁面檔案。 第一次實體記憶體壓力偏高時,作業系統必須將空間放在實體記憶體中以儲存資料,並將實體記憶體中的部分資料備份到頁面檔案。 資料在需要之前不會進行分頁,因此在實體記憶體壓力偏低的情況下,可能會發生分頁。

記憶體配置

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

從 Managed 堆積中配置記憶體要比 Unmanaged 記憶體配置快。 由於執行時間會藉由將值新增至指標來設定物件的記憶體,因此幾乎與從堆疊配置記憶體一樣快。 此外,由於連續配置的新物件會連續儲存在 Managed 堆積中,因此應用程式可以快速存取物件。

記憶體釋放

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

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

只有在集合發現大量無法連線的物件時,才會壓縮記憶體。 如果 Managed 堆積中的所有物件都存留集合,則不需要記憶體壓縮。

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

記憶體回收的條件

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

  • 系統的實體記憶體不足。 記憶體大小是由作業系統的記憶體不足通知或主機所指示的記憶體不足所偵測。

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

  • 即會呼叫 GC.Collect 方法。 在幾乎所有情況下,您不需要呼叫此方法,因為垃圾收集行程會持續執行。 這個方法主要用於獨特的情況和測試。

Managed 堆積

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

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

若要保留記憶體,垃圾收集行程會呼叫 Windows VirtualAlloc 函式,並一次為受控應用程式保留一個記憶體區段。 垃圾收集行程也會視需要保留區段,並藉由呼叫 Windows VirtualFree 函式,將區段釋放回作業系統 () 。

重要事項

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

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

觸發垃圾收集時,垃圾收集行程會回收死物件所佔用的記憶體。 回收程式會壓縮即時物件,使其一起移動,並移除死空間,進而讓堆積變小。 此程式可確保一起配置的物件會保持在 Managed 堆積上,以保留其位置。

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

堆積可視為兩個堆積的累積: 大型物件堆積 和小型物件堆積。 大型物件堆積包含 85,000 個位元組和更大的物件,通常是陣列。 實例物件很少會超大型。

提示

您可以設定物件在大型物件堆積上的 臨界值大小

層代

GC 演算法是以數個考慮為基礎:

  • 比起整個 Managed 堆積,壓縮一部分 Managed 堆積的記憶體會更快。
  • 較新的物件具有較短的存留期,而較舊的物件具有較長的存留期。
  • 較新的物件通常會彼此相關,並同時由應用程式存取。

垃圾收集主要發生在短期物件的回收中。 為了優化垃圾收集行程的效能,Managed 堆積分成三個層代 0、1 和 2,因此可以個別處理長期和短期的物件。 垃圾收集行程會將新物件儲存在層代 0 中。 在應用程式存留期初期建立的物件則會升階並儲存在第 1 個和第 2 個層代。 因為壓縮部分 Managed 堆積的速度比整個堆積快,所以此配置可讓垃圾收集行程在特定世代中釋放記憶體,而不是每次執行收集時釋放整個 Managed 堆積的記憶體。

  • 層代 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 物件的控制碼,可由使用者程式碼或 Common Language Runtime 配置。

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

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

下圖顯示觸發垃圾收集的執行緒,並導致其他執行緒暫停:

執行緒如何觸發垃圾收集的螢幕擷取畫面。

非受控資源

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

當您定義封裝 Unmanaged 資源的物件時,建議您提供必要的程式碼,以清除公用 Dispose 方法中的 Unmanaged 資源。 藉由提供 Dispose 方法,您可以讓使用者在物件完成時明確釋放資源。 當您使用封裝 Unmanaged 資源的物件時,請務必視需要呼叫 Dispose

如果類型的取用者忘記呼叫 Dispose ,您也必須提供一種方式來釋放 Unmanaged 資源。 您可以使用安全控制碼來包裝 Unmanaged 資源,或覆寫 Object.Finalize() 方法。

如需清除 Unmanaged 資源的詳細資訊,請參閱 清除 Unmanaged 資源

另請參閱