自動記憶體管理是 Common Language Runtime 在 Managed Execution 期間所提供的其中一項服務。 Common Language Runtime 的垃圾回收器會管理應用程式的記憶體配置和釋放。 對於開發人員而言,這表示您不需要撰寫程式代碼,即可在開發受控應用程式時執行記憶體管理工作。 自動記憶體管理可以消除常見問題,例如忘記釋放物件並造成記憶體流失,或嘗試存取已釋放之物件的記憶體。 本節介紹垃圾收集器如何分配和釋放記憶體。
配置記憶體
當您初始化新的處理序 (Process) 時,Runtime 會保留一塊連續的位址空間區域,供處理序使用。 這塊保留的位址空間稱為 Managed Heap。 受控堆積會維護一個指標,指向下個將在堆積中配置的物件的位址。 初始,這個指標會被設定為受管理堆積的基底位址。 所有 參考類型 都會配置在受控堆上。 當應用程式建立第一個參考型別時,會在受控堆積的基底位址為該型別配置記憶體。 當應用程式建立下一個物件時,垃圾收集器會在緊接著第一個物件之後的位址空間中配置記憶體。 只要位址空間可用,垃圾收集行程就會以這種方式持續為新物件分配空間。
從受控堆中分配記憶體比非受控記憶體分配要快。 因為執行階段會藉由將值加到指標上來配置物件的記憶體,所以它幾乎和從堆疊配置記憶體一樣快。 此外,因為新物件會被連續儲存在託管堆積中,所以應用程式能快速地存取這些物件。
釋放記憶體
垃圾回收器的優化引擎會根據記憶體配置,決定執行回收的最佳時機。 當垃圾回收器執行回收時,會釋放應用程式已不再使用的物件所佔用的記憶體。 它會藉由檢查應用程式的根目錄,判斷不再使用哪些物件。 每個應用程式都有一組根目錄。 每個根不是指向 Managed 堆中的物件,就是設為 Null。 應用程式的根目錄包括靜態字段、線程堆疊上的局部變數和參數,以及 CPU 快取器。 垃圾收集器可以存取 JIT 編譯器 和執行階段維護的活動根目錄清單。 使用此清單,它會檢查應用程式的根目錄,並在程式中建立圖形,其中包含可從根存取的所有物件。
不在圖形中的物件無法從應用程式的根目錄連線。 垃圾收集器會將無法到達的物件視為垃圾,並釋放其佔用的記憶體。 在回收期間,記憶體回收行程會查看受控集堆,尋找不可達物件所佔用的位址區塊。 每找到一個無法取得的物件時,它便會使用記憶體複製功能,壓縮記憶體中可取得的物件,然後釋放出為無法取得的物件所配置的位址空間區塊。 一旦壓縮完可達物件的記憶體之後,垃圾回收器會進行必要的指標更正,使應用程式的根指向物件的新位置。 它也會將受控堆積的指標調整到最後可存取物件的後面。 請注意,只有在集合發現大量不可達的物件時,記憶體才會被壓縮。 如果受控堆疊中的所有物件在垃圾回收後依然存在,則不需要記憶體整理。
為了提高效能,執行階段會將大型物件的記憶體配置到獨立堆積區中。 垃圾收集器會自動釋放大型物件的記憶體。 不過,為了避免在記憶體中移動大型物件,不會壓縮此記憶體。
世代和效能
為了優化垃圾回收器的效能,管理堆積分成三個世代:0、1 和 2。 執行期的垃圾回收演算法是基於電腦軟體產業在實驗不同垃圾回收方案時發現的幾個概括性結論。 首先,壓縮受控堆積中部分記憶體比壓縮整個受控堆積更快。 其次,較新的物件會有較短的存留期,而較舊的物件會有較長的存留期。 最後,較新的物件通常會彼此相關,並由應用程式在同一時間存取。
執行期的垃圾收集器會將新物件儲存在第0代中。 在應用程式生命週期初期建立且在垃圾回收中存活的物件會被提升並儲存在第 1 代和第 2 代中。 在本主題中稍後將描述物件升級的流程。 由於壓縮部分受控堆的速度比整個堆還快,此機制可讓垃圾收集器在特定世代中釋放記憶體,而不必每次執行收集時釋放整個受控堆的記憶體。
事實上,當代 0 已滿時,垃圾回收器會執行回收作業。 如果應用程式在第 0 代已滿時嘗試建立新的物件,垃圾回收器會發現第 0 代中沒有剩餘地址空間來配置給物件。 垃圾收集行程會嘗試為 物件釋放層代 0 中的地址空間,以執行收集。 垃圾收集器從檢查第 0 代中的物件開始,而不是檢查受控堆中所有的物件。 這是最有效率的方法,因為新物件通常會有較短的存留期,而且預期執行集合時,應用程式不會再使用第0代中的許多物件。 此外,單靠層代 0 的集合通常會回收足夠的記憶體,讓應用程式繼續建立新的物件。
垃圾收集器在進行第 0 代的收集之後,它會壓縮可達物件的記憶體,如本主題先前所述的 釋放記憶體。 垃圾收集器接著會升級這些物件,並將受控堆的這個部分視為第 1 代。 由於回收之後存留下來的物件通常具有較長的存留期,因此才會將它們提升至較高的層代。 因此,垃圾收集器在每次執行第 0 代的收集時,不需要重新檢查第 1 代和第 2 代中的物件。
在垃圾收集器執行第一次第 0 代的收集並將可到達的物件提升到第 1 代之後,其餘的託管堆積被視為第 0 代。 它會繼續為層代 0 中的新物件配置記憶體,直到層代 0 已滿,而且必須執行另一個集合。 此時,垃圾回收器的優化引擎會判斷是否需要檢查較舊世代中的物件。 例如,如果第 0 代的集合未回收足夠的記憶體,讓應用程式順利完成建立新對象的嘗試,垃圾收集器可以執行第 1 代的集合,然後再執行第 2 代的集合。 如果這無法回收足夠的記憶體,垃圾回收器可以執行第 2、1 和 0 代的收集。 每次收集之後,垃圾回收器會整理世代 0 中的可達物件,並將其升階為世代 1。 在第 1 代中經過垃圾回收後仍然存活的物件會被提升至第 2 代。 因為垃圾回收器只支援三個世代,所以在第 2 代中倖存下來的物件會保留在第 2 代中,直到它們在未來的回收中被確定無法被存取為止。
釋放非受控資源的記憶體
針對應用程式所建立的大部分物件,您可以依賴垃圾收集器自動執行必要的記憶體管理工作。 但是,非管理資源需要明確清除。 最常見的非受控資源類型是包裝作業系統資源的物件,例如檔案控制代碼、視窗控制代碼或網路連接。 雖然垃圾收集器能夠追蹤封裝非管理資源之管理物件的存留期,但它並沒有具體知識來清除該資源。 當您建立封裝 Unmanaged 資源的物件時,建議您提供必要的程式代碼,以在公用 Dispose 方法中清除 Unmanaged 資源。 藉由提供 Dispose 方法,您可以讓用戶在物件完成時明確釋放其記憶體。 當您使用封裝非受控資源的物件時,您應該注意 Dispose 並在需要時呼叫它。 如需有關清理非受控資源的詳細資訊,以及實作 Dispose 的設計模式範例,請參閱 記憶體管理。