垃圾回收(一)【垃圾回收的基礎】

風靈使發表於2019-01-07

垃圾回收的基礎

在公共語言執行時 (CLR) 中,垃圾回收器用作自動記憶體管理器。 它提供如下優點:

  • 使你可以在開發應用程式時不必釋放記憶體。

  • 有效分配託管堆上的物件。

  • 回收不再使用的物件,清除它們的記憶體,並保留記憶體以用於將來分配。 託管物件會自動獲取乾淨的內容來開始,因此,它們的建構函式不必對每個資料欄位進行初始化。

  • 通過確保物件不能使用另一個物件的內容來提供記憶體安全。

本主題介紹垃圾回收的核心概念。 它包含下列部分:

記憶體基礎知識

下面的列表總結了重要的 CLR 記憶體概念。

  • 每個程式都有其自己單獨的虛擬地址空間。 同一臺計算機上的所有程式共享相同的實體記憶體,如果有頁檔案,則也共享頁檔案。

  • 預設情況下,32 位計算機上的每個程式都具有 2GB 的使用者模式虛擬地址空間。

  • 作為一名應用程式開發人員,你只能使用虛擬地址空間,請勿直接操控實體記憶體。 垃圾回收器為你分配和釋放託管堆上的虛擬記憶體。

    如果你編寫的是本機程式碼,請使用 Win32 函式處理虛擬地址空間。 這些函式為你分配和釋放本機堆上的虛擬記憶體。

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

    • 可用。 該記憶體塊沒有引用關係,可用於分配。

    • 保留。 記憶體塊可供你使用,並且不能用於任何其他分配請求。 但是,在該記憶體塊提交之前,你無法將資料儲存到其中。

    • 提交。 記憶體塊已指派給物理儲存。

  • 可能會存在虛擬地址空間碎片。 就是說地址空間中存在一些被稱為孔的可用塊。 當請求虛擬記憶體分配時,虛擬記憶體管理器必須找到滿足該分配請求的足夠大的單個可用塊。 即使有 2GB 可用空間,2GB 分配請求也會失敗,除非所有這些可用空間都位於一個地址塊中。

  • 如果用完保留的虛擬地址空間或提交的物理空間,則可能會用盡記憶體。

即使在實體記憶體壓力(即實體記憶體的需求)較低的情況下也會使用頁檔案。 首次出現實體記憶體壓力較高的情況時,作業系統必須在實體記憶體中騰出空間來儲存資料,並將實體記憶體中的部分資料備份到頁檔案中。 該資料只會在需要時進行分頁,所以在實體記憶體壓力非常低的情況下也可能會進行分頁。

垃圾回收的條件

當滿足以下條件之一時將發生垃圾回收:

  • 系統具有低的實體記憶體。 這是通過 OS 的記憶體不足通知或主機指示的記憶體不足檢測出來。

  • 由託管堆上已分配的物件使用的記憶體超出了可接受的閾值。 隨著程式的執行,此閾值會不斷地進行調整。

  • 呼叫System.GC.Collect方法。 幾乎在所有情況下,你都不必呼叫此方法,因為垃圾回收器會持續執行。此方法主要用於特殊情況和測試。

託管堆

在垃圾回收器由 CLR 初始化之後,它會分配一段記憶體用於儲存和管理物件。 此記憶體稱為託管堆(與作業系統中的本機堆相對)。

每個託管程式都有一個託管堆。 程式中的所有執行緒都在同一堆上分配物件記憶。

若要保留記憶體,垃圾回收器會呼叫 Win32 VirtualAlloc 函式,並且每次為託管應用保留一個記憶體段。 垃圾回收器還會根據需要保留記憶體段,並呼叫 Win32 VirtualFree 函式,將記憶體段釋放回作業系統(在清除所有物件的記憶體段後)。

垃圾回收器分配的段大小特定於實現,並且隨時可能更改(包括定期更新)。 應用程式不應假設特定段的大小或依賴於此大小,也不應嘗試配置段分配可用的記憶體量。

堆上分配的物件越少,垃圾回收器必須執行的工作就越少。 分配物件時,請勿使用超出你需求的舍入值,例如在僅需要 15 個位元組的情況下分配了 32 個位元組的陣列。

當觸發垃圾回收時,垃圾回收器將回收由死物件佔用的記憶體。 回收程式會對活動物件進行壓縮,以便將它們一起移動,並移除死空間,從而使堆更小一些。 這將確保一起分配的物件全都位於託管堆上,從而保留它們的區域性性。

垃圾回收的侵入性(頻率和持續時間)是由分配的數量和託管堆上保留的記憶體數量決定的。

此堆可視為兩個堆的累計:大物件堆和小物件堆。

大物件堆包含其大小為 85,000 個位元組和更多位元組的物件。 大物件堆上的物件通常是陣列。 非常大的例項物件是很少見的。

代數

堆按代進行組織,因此它可以處理長生存期的物件和短生存期的物件。 垃圾回收主要在回收通常只佔用一小部分堆的短生存期物件時發生。 堆上的物件有三代:

  • 第 0 代。 這是最年輕的代,其中包含短生存期物件。 短生存期物件的一個示例是臨時變數。 垃圾回收最常發生在此代中。

    新分配的物件構成新一代的物件並且為隱式的第 0 代回收,除非它們是大物件,在這種情況下,它們將進入第 2 代回收中的大物件堆。

    大多數物件通過第 0 代中的垃圾回收進行回收,不會保留到下一代。

  • 第 1 代。 這一代包含短生存期物件並用作短生存期物件和長生存期物件之間的緩衝區。

  • 第 2 代。 這一代包含長生存期物件。 長生存期物件的一個示例是伺服器應用程式中的一個包含在程式期間處於活動狀態的靜態資料的物件。

當條件得到滿足時,垃圾回收將在特定代上發生。 回收某個代意味著回收此代中的物件及其所有更年輕的代。 第 2 代垃圾回收也稱為完整垃圾回收,因為它回收所有代上的所有物件(即,託管堆中的所有物件)。

倖存和提升

垃圾回收中未回收的物件也稱為倖存者,並會被提升到下一代。在第 0 代垃圾回收中倖存的物件將被提升到第 1 代;在第 1 代垃圾回收中倖存的物件將被提升到第 2 代;而在第 2 代垃圾回收中倖存的物件將仍為第 2 代。

當垃圾回收器檢測到某個代中的倖存率很高時,它會增加該代的分配閾值,因此下一次回收將會獲取一個非常大的回收記憶體。 CLR 會在以下兩個優先順序別之前進行平衡:不允許應用程式的工作集獲取太大記憶體以及不允許垃圾回收花費太多時間。

暫時代和暫時段

因為第 0 代和第 1 代中的物件的生存期較短,因此,這些代被稱為暫時代。

暫時代必須在稱為暫時段的記憶體段中進行分配。 垃圾回收器獲取的每個新段將成為新的暫時段,幷包含在第 0 代垃圾回收中倖存的物件。 舊的暫時段將成為新的第 2 代段。

根據系統為 32 位還是 64 位以及它正在哪種型別的垃圾回收器上執行,暫時段的大小發生相應變化。 下表列出了預設值。

32 位 64 位
工作站 GC 16 MB 256 MB
伺服器 GC 64 MB 4 GB
伺服器 GC(具有 4 個以上的邏輯 CPU) 32 MB 2 GB
伺服器 GC(具有 8 個以上的邏輯 CPU) 16 MB 1 GB

暫時段可以包含第 2 代物件。 第 2 代物件可使用多個段(在記憶體允許的情況下程式所需的任意數量)。

從暫時垃圾回收中釋放的記憶體量限制為暫時段的大小。 釋放的記憶體量與死物件佔用的空間成比例。

垃圾回收過程中發生的情況

垃圾回收分為以下幾個階段:

  • 標記階段,找到並建立所有活動物件的列表。

  • 重定位階段,用於更新對將要壓縮的物件的引用。

  • 壓縮階段,用於回收由死物件佔用的空間,並壓縮倖存的物件。 壓縮階段將垃圾回收中倖存下來的物件移至段中時間較早的一端。

    因為第 2 代回收可以佔用多個段,所以可以將已提升到第 2 代中的物件移動到時間較早的段中。 可以將第 1 代倖存者和第 2 代倖存者都移動到不同的段,因為它們已被提升到第 2 代。

    通常,由於複製大型物件會造成效能代償,因此不會壓縮大型物件堆。 但是,從net_v451開始,你可以使用 System.Runtime.GCSettings.LargeObjectHeapCompactionMode屬性按需壓縮大物件堆。

垃圾回收器使用以下資訊來確定物件是否為活動物件:

  • 堆疊根。 由實時 (JIT) 編譯器和堆疊檢視器提供的堆疊變數。

  • 垃圾回收控制程式碼。 指向託管物件且可由使用者程式碼或公共語言執行時分配的控制程式碼。

  • 靜態資料。 應用程式域中可能引用其他物件的靜態物件。 每個應用程式域都會跟蹤其靜態物件。

在垃圾回收啟動之前,除了觸發垃圾回收的執行緒以外的所有託管執行緒均會掛起。

下圖演示了觸發垃圾回收並導致其他執行緒掛起的執行緒。

觸發垃圾回收的執行緒
當執行緒觸發垃圾回收時

操作非託管資源

如果你的託管物件使用非託管物件的本機檔案控制程式碼來引用非託管物件,則必須顯式釋放非託管物件,因為垃圾回收器僅跟蹤託管堆上的記憶體。

託管物件的使用者可能不會釋放由該物件使用的本機資源。 為了執行清理,可以使託管物件成為可終結的。 終結由不再使用物件時執行的清理操作組成。 當託管物件不活動時,它將執行在其終結器方法中指定的清理操作。

當發現某個可終結物件處於不活動狀態時,則會將其終結器放入佇列中,以便執行其清理操作,但要將該物件自身提升到下一代。 因此,你必須等待該代上發生下一次垃圾回收(並不一定是下一次垃圾回收),以確定物件是否已收回。

工作站和伺服器垃圾回收

垃圾回收器可自行優化並且適用於多種方案。 你可使用配置檔案設定來基於工作負荷的特徵設定垃圾回收的型別。 CLR 提供了以下型別的垃圾回收:

  • 工作站垃圾回收,用於所有客戶端工作站和獨立 PC。 這是執行時配置架構中 <gcServer> 元素的預設設定。

    工作站垃圾回收既可以是併發的,也可以是非併發的。 併發垃圾回收使託管執行緒能夠在垃圾回收期間繼續操作。

    net_v40_long開始,後臺垃圾回收取代了併發垃圾回收。

  • 伺服器垃圾回收,用於需要高吞吐量和可伸縮性的伺服器應用程式。 伺服器垃圾回收既可以是非併發也可以是背景。

下圖演示了伺服器上執行垃圾回收的專用執行緒。

伺服器垃圾回收

伺服器垃圾回收執行緒

配置垃圾回收

可以使用執行時配置架構的 <gcServer> 元素,指定要 CLR 執行的垃圾回收型別。 在將此元素的 enabled 特性設定為 false (預設值)時,CLR 將執行工作站垃圾回收。 在將 enabled 特性設定為 true時,CLR 將執行伺服器垃圾回收。

併發垃圾回收是使用執行時配置架構的 <gcConcurrent> 元素進行指定。 預設設定為 enabled。此設定可控制併發和後臺垃圾回收。

還可以使用非託管承載介面來指定伺服器垃圾回收。 請注意,如果你的應用程式承載在這些環境之一中,則 ASP.NETSQL Server 將自動啟用伺服器垃圾回收。

工作站和伺服器垃圾回收比較

以下是工作站垃圾回收的執行緒處理和效能注意事項:

  • 回收發生在觸發垃圾回收的使用者執行緒上,並保留相同優先順序。 因為使用者執行緒通常以普通優先順序執行,所以垃圾回收器(在普通優先順序執行緒上執行)必須與其他執行緒競爭 CPU 時間。

    不會掛起執行本機程式碼的執行緒。

  • 工作站垃圾回收始終用於只有一個處理器的計算機,無論<gcServer>設定如何。 如果你指定伺服器垃圾回收,則 CLR 會使用工作站垃圾回收,並禁用併發。

以下是伺服器垃圾回收的執行緒處理和效能注意事項:

  • 回收發生在以 THREAD_PRIORITY_HIGHEST 優先順序執行的多個專用執行緒上。

  • 為每個 CPU 提供一個用於執行垃圾回收的一個堆和專用執行緒,並將同時回收這些堆。 每個堆都包含一個小物件堆和一個大物件堆,並且所有的堆都可由使用者程式碼訪問。 不同堆上的物件可以相互引用。

  • 因為多個垃圾回收執行緒一起工作,所以對於相同大小的堆,伺服器垃圾回收比工作站垃圾回收更快一些。

  • 伺服器垃圾回收通常具有更大的段。 但是請注意,這是通常情況:段大小特定於實現且可能更改。 調整應用程式時,不應假設垃圾回收器分配的段大小。

  • 伺服器垃圾回收會佔用大量資源。 例如,如果在一臺具有 4 個處理器的計算機上執行了 12 個程式,則在它們都使用伺服器垃圾回收的情況下,將有 48 個專用垃圾回收執行緒。 在高記憶體載入的情況下,如果所有程式開始執行垃圾回收,則垃圾回收器將要計劃 48 個執行緒。

如果執行應用程式的數百個例項,請考慮使用工作站垃圾回收並禁用併發垃圾回收。 這可以減少上下文切換,從而提高效能。

並行垃圾回收

在工作站或伺服器垃圾回收中,你可以啟用併發垃圾回收,以便在大多數回收期間,讓各執行緒與執行垃圾回收的專用執行緒併發執行。 此選項隻影響第 2 代中的垃圾回收;第 0 代和第 1 代中的垃圾回收始終是非併發的,因為它們完成的速度非常快。

併發垃圾回收通過最大程度地減少因回收引起的暫停,使互動應用程式能夠更快地響應。 在執行併發垃圾回收執行緒的大多數時間,託管執行緒可以繼續執行。 這可以使得在發生垃圾回收時的暫停時間更短。

若要在執行多個程式時提高效能,請禁用併發垃圾回收。 為此,可以將 <gcConcurrent> 元素新增到應用的配置檔案,並將其 enabled 屬性的值設定為 "false"

併發垃圾回收在一個專用執行緒上執行。 預設情況下,CLR 將執行工作站垃圾回收並啟用併發垃圾回收。 對於單處理器計算機和多處理器計算機都是如此。

你在併發垃圾回收期間在堆上為小物件分配空間的能力將受到在併發垃圾回收啟動時暫時段上保留的物件的限制。 一旦到達暫時段的末尾,將必須等待併發垃圾回收完成,同時將掛起需要執行小物件分配的託管執行緒。

併發垃圾回收具有一個稍微大點的工作集(與非併發垃圾回收相比),這是因為你可以在併發回收期間分配物件。 但是,這會影響效能,原因是分配的物件將會成為你的工作集的一部分。 實質上,併發垃圾回收會犧牲一些 CPU 和記憶體來換取更短的暫停。

下圖演示了在單獨的專用執行緒上執行的併發垃圾回收。

並行垃圾回收
併發垃圾回收執行緒

後臺工作站垃圾回收

在後臺垃圾回收中,在進行第 2 代回收的過程中,將會根據需要收集暫時代(第 0 代和第 1 代)。 後臺垃圾回收無法設定;它會自動執行並啟用併發垃圾回收。 後臺垃圾回收是對併發垃圾回收的替代。與併發垃圾回收一樣,後臺垃圾回收是在一個專用執行緒上執行的並且只適用於第 2 代回收。

後臺垃圾回收只在net_v40_short及更高版本中可用。 在 net_v40_short中,僅支援工作站垃圾回收。 從 .NET Framework 4.5 開始,後臺垃圾回收可用於工作站和伺服器垃圾回收。

後臺垃圾回收期間對暫時代的回收稱為前臺垃圾回收。 發生前臺垃圾回收時,所有託管執行緒都將被掛起。

當後臺垃圾回收正在進行並且你已在第 0 代中分配了足夠的物件時,CLR 將執行第 0 代或第 1 代前臺垃圾回收。專用的後臺垃圾回收執行緒將在常見的安全點上進行檢查以確定是否存在對前臺垃圾回收的請求。 如果存在,則後臺回收將掛起自身以便前臺垃圾回收可以發生。在前臺垃圾回收完成之後,專用的後臺垃圾回收執行緒和使用者執行緒將繼續。

後臺垃圾回收可以消除併發垃圾回收所帶來的分配限制,因為在後臺垃圾回收期間,可發生暫時垃圾回收。 這意味著,後臺垃圾回收可以移除暫時代中的死物件,而且還可以在第 1 代垃圾回收期間根據需要展開堆。

下圖顯示對工作站上的獨立專用執行緒執行的後臺垃圾回收。

在這裡插入圖片描述
後臺工作站垃圾回收

後臺伺服器垃圾回收

從 .NET Framework 4.5 開始,後臺伺服器垃圾回收是伺服器垃圾回收的預設模式。 若要選擇此模式,請在執行時配置架構中將 <gcServer> 元素的 enabled 屬性設定為 true。 此模式與後臺工作站垃圾回收(如上一章節所描述)具有類似功能,但有一些不同之處。 後臺工作區域垃圾回收使用一個專用的後臺垃圾回收執行緒,而後臺伺服器垃圾回收使用多個執行緒,通常一個專用的執行緒用於一臺邏輯處理器。 不同於工作站後臺垃圾回收執行緒,這些執行緒不會超時。

下圖顯示對伺服器上的獨立專用執行緒執行的後臺垃圾回收。
在這裡插入圖片描述
後臺伺服器垃圾回收

相關文章