CLR垃圾回收和性能

垃圾回收的基本知識

在公共語言運行時 (CLR) 中,垃圾回收器 (GC) 用作自動內存管理器。 垃圾回收器管理應用程序的內存分配和釋放。 因此,使用託管代碼的開發人員無需編寫執行內存管理任務的代碼。 自動內存管理可解決常見問題,例如,忘記釋放對象並導致內存泄漏,或嘗試訪問已釋放對象的已釋放內存。

本文章介紹垃圾回收的核心概念。

優點

垃圾回收器具有以下優點:

  • 開發人員不必手動釋放內存。

  • 有效分配託管堆上的對象。

  • 回收不再使用的對象,清除它們的內存,並保留內存以用於將來分配。 託管對象會自動獲取乾淨的內容來開始,因此,它們的構造函數不必對每個數據字段進行初始化。

  • 通過確保對象不能自己使用分配給另一個對象的內存來提供內存安全。

內存基礎知識

下面的列表總結了重要的 CLR 內存概念:

  • 每個進程都有其自己單獨的虛擬地址空間。 同一臺計算機上的所有進程共享相同的物理內存和頁文件(如果有)。

  • 默認情況下,32 位計算機上的每個進程都具有 2 GB 的用戶模式虛擬地址空間。

  • 作爲一名應用程序開發人員,你只能使用虛擬地址空間,請勿直接操控物理內存。 垃圾回收器爲你分配和釋放託管堆上的虛擬內存。

    如果你編寫的是本機代碼,請使用 Windows 函數處理虛擬地址空間。 這些函數爲你分配和釋放本機堆上的虛擬內存。

  • 虛擬內存有三種狀態:

    狀態描述
    Free 該內存塊沒有引用關係,可用於分配。
    保留 內存塊可供你使用,不能用於任何其他分配請求。 但是,在該內存塊提交之前,你無法將數據存儲到其中。
    已提交 內存塊已指派給物理存儲。
  • 可能會存在虛擬地址空間碎片,這意味着地址空間中存在一些被稱爲孔的可用塊。 當請求虛擬內存分配時,虛擬內存管理器必須找到滿足該分配請求的足夠大的單個可用塊。 即使有 2 GB 可用空間,2 GB 分配請求也會失敗,除非所有這些可用空間都位於一個地址塊中。

  • 如果沒有足夠的可供保留的虛擬地址空間或可供提交的物理空間,則可能會用盡內存。

    即使在物理內存壓力(物理內存的需求)較低的情況下也會使用頁文件。 首次出現物理內存壓力較高的情況時,操作系統必須在物理內存中騰出空間來存儲數據,並將物理內存中的部分數據備份到頁文件中。 該數據只會在需要時進行分頁,所以在物理內存壓力較低的情況下也可能會進行分頁。

內存分配

初始化新進程時,運行時會爲進程保留一個連續的地址空間區域。 這個保留的地址空間被稱爲託管堆。 託管堆維護着一個指針,用它指向將在堆中分配的下一個對象的地址。 最初,該指針設置爲指向託管堆的基址。 託管堆上部署了所有引用類型。 應用程序創建第一個引用類型時,將爲託管堆的基址中的類型分配內存。 應用程序創建下一個對象時,垃圾回收器在緊接第一個對象後面的地址空間內爲它分配內存。 只要地址空間可用,垃圾回收器就會繼續以這種方式爲新對象分配空間。

從託管堆中分配內存要比非託管內存分配速度快。 由於運行時通過爲指針添加值來爲對象分配內存,所以這幾乎和從堆棧中分配內存一樣快。 另外,由於連續分配的新對象在託管堆中是連續存儲,所以應用程序可以快速訪問這些對象。

內存釋放

垃圾回收器的優化引擎根據所執行的分配決定執行回收的最佳時間。 垃圾回收器在執行回收時,會釋放應用程序不再使用的對象的內存。 它通過檢查應用程序的根來確定不再使用的對象。 應用程序的根包含線程堆棧上的靜態字段、局部變量、CPU 寄存器、GC 句柄和終結隊列。 每個根或者引用託管堆中的對象,或者設置爲空。 垃圾回收器可以爲這些根請求其餘運行時。 垃圾回收器使用此列表創建一個圖表,其中包含所有可從這些根中訪問的對象。

不在該圖表中的對象將無法從應用程序的根中訪問。 垃圾回收器會考慮無法訪問的對象垃圾,並釋放爲它們分配的內存。 在回收中,垃圾回收器檢查託管堆,查找無法訪問對象所佔據的地址空間塊。 發現無法訪問的對象時,它就使用內存複製功能來壓縮內存中可以訪問的對象,釋放分配給不可訪問對象的地址空間塊。 在壓縮了可訪問對象的內存後,垃圾回收器就會做出必要的指針更正,以便應用程序的根指向新地址中的對象。 它還將託管堆指針定位至最後一個可訪問對象之後。

只有在回收發現大量的無法訪問的對象時,纔會壓縮內存。 如果託管堆中的所有對象均未被回收,則不需要壓縮內存。

爲了改進性能,運行時爲單獨堆中的大型對象分配內存。 垃圾回收器會自動釋放大型對象的內存。 但是,爲了避免移動內存中的大型對象,通常不會壓縮此內存。

垃圾回收的條件

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

  • 系統具有低的物理內存。 內存大小是通過操作系統的內存不足通知或主機指示的內存不足檢測出來的。

  • 由託管堆上已分配的對象使用的內存超出了可接受的閾值。 隨着進程的運行,此閾值會不斷地進行調整。

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

託管堆

在 CLR 初始化垃圾回收器後,它會分配一段內存用於存儲和管理對象。 此內存稱爲託管堆(與操作系統中的本機堆相對)。

每個託管進程都有一個託管堆。 進程中的所有線程都在同一堆上爲對象分配內存。

若要保留內存,垃圾回收器會調用 Windows VirtualAlloc 函數,並且每次爲託管應用保留一個內存段。 垃圾回收器還會根據需要保留內存段,並調用 Windows VirtualFree 函數,將內存段釋放回操作系統(在清除所有對象的內存段後)。

 重要

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

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

當觸發垃圾回收時,垃圾回收器將回收由非活動對象佔用的內存。 回收進程會對活動對象進行壓縮,以便將它們一起移動,並移除死空間,從而使堆更小一些。 此進程可確保一起分配的對象全都位於託管堆上,從而保留它們的局部性。

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

此堆可視爲兩個堆的累計:大對象堆和小對象堆。 大對象堆包含大小不少於 85,000 個字節的對象,這些對象通常是數組。 非常大的實例對象是很少見的。

 提示

可以配置閾值大小,以使對象能夠進入大型對象堆。

代數

GC 算法基於幾個注意事項:

  • 壓縮託管堆的一部分內存要比壓縮整個託管堆速度快。
  • 較新的對象生存期較短,而較舊的對象生存期則較長。
  • 較新的對象趨向於相互關聯,並且大致同時由應用程序訪問。

垃圾回收主要在回收短生存期對象時發生。 爲優化垃圾回收器的性能,將託管堆分爲三代:第 0 代、第 1 代和第 2 代,因此它可以單獨處理長生存期和短生存期對象。 垃圾回收器將新對象存儲在第 0 代中。 在應用程序生存期的早期創建的對象如果未被回收,則被升級並存儲在第 1 級和第 2 級中。 因爲壓縮託管堆的一部分要比壓縮整個託管堆速度快,所以此方案允許垃圾回收器在每次執行回收時釋放特定級別的內存,而不是整個託管堆的內存。

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

    新分配的對象構成新一代對象,並隱式地成爲第 0 代集合。 但是,如果它們是大型對象,它們將延續到大型對象堆 (LOH),這有時稱爲第 3 代。 第 3 代是在第 2 代中邏輯收集的物理生成。

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

    如果應用程序在第 0 代託管堆已滿時嘗試創建新對象,垃圾回收器將執行收集,爲該對象釋放地址空間。 垃圾回收器從檢查第 0 級託管堆中的對象(而不是託管堆中的所有對象)開始執行回收。 單獨回收第 0 代託管堆通常可以回收足夠的內存,這樣,應用程序便可以繼續創建新對象。

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

    垃圾回收器執行第 0 代託管堆的回收後,會壓縮可訪問對象的內存,並將其升級到第 1 代。 因爲未被回收的對象往往具有較長的生存期,所以將它們升級至更高的級別很有意義。 垃圾回收器不必在每次執行第 0 代託管堆的回收時,都重新檢查第 1 代和第 2 代託管堆中的對象。

    如果第 0 代託管堆的回收沒有回收足夠的內存供應用程序創建新對象,垃圾回收器就會先執行第 1 代託管堆的回收,然後再執行第 2 代託管堆的回收。 第 1 級託管堆中未被回收的對象將會升級至第 2 級託管堆。

  • 第 2 代:這一代包含長生存期對象。 長生存期對象的一個示例是服務器應用程序中的一個包含在進程期間處於活動狀態的靜態數據的對象。

    第 2 代託管堆中未被回收的對象會繼續保留在第 2 代託管堆中,直到在將來的回收中確定它們無法訪問爲止。

    大型對象堆上的對象(有時稱爲 第 3 代)也在第 2 代中收集。

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

倖存和提升

垃圾回收中未回收的對象也稱爲倖存者,並會被提升到下一代:

  • 第 0 代垃圾回收中未被回收的對象將會升級至第 1 代。
  • 第 1 代垃圾回收中未被回收的對象將會升級至第 2 代。
  • 第 2 代垃圾回收中未被回收的對象將仍保留在第 2 代。

當垃圾回收器檢測到某個代中的倖存率很高時,它會增加該代的分配閾值。 下次回收將回收非常大的內存。 CLR 持續在以下兩個優先級之間進行平衡:不允許通過延遲垃圾回收,讓應用程序的工作集獲取太大內存,以及不允許垃圾回收過於頻繁地運行。

暫時代和暫時段

因爲第 0 代和第 1 代中的對象的生存期較短,因此,這些代被稱爲“暫時代”。

暫時代在稱爲“暫時段”的內存段中進行分配。 垃圾回收器獲取的每個新段將成爲新的暫時段,幷包含在第 0 代垃圾回收中倖存的對象。 舊的暫時段將成爲新的第 2 代段。

根據系統爲 32 位還是 64 位以及它正在哪種類型的垃圾回收器(工作站或服務器 GC)上運行,暫時段的大小發生相應變化。 下表顯示了暫時段的默認大小:

工作站/服務器 GC32 位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 代。

    通常,由於複製大型對象會造成性能下降,因此不會壓縮大型對象堆 (LOH)。 但是,在 .NET Core 和 .NET Framework 4.5.1 及更高版本中,可以根據需要使用 GCSettings.LargeObjectHeapCompactionMode 屬性按需壓縮大型對象堆。 此外,當通過指定以下任一項設置硬限制時,將自動壓縮 LOH:

垃圾回收器使用以下信息來確定對象是否爲活動對象:

  • 堆棧根:由實時 (JIT) 編譯器和堆棧查看器提供的堆棧變量。 JIT 優化可以延長或縮短報告給垃圾回收器的堆棧變量內的代碼的區域。

  • 垃圾回收句柄:指向託管對象且可由用戶代碼或公共語言運行時分配的句柄。

  • 靜態數據:應用程序域中可能引用其他對象的靜態對象。 每個應用程序域都會跟蹤其靜態對象。

在垃圾回收啓動之前,除了觸發垃圾回收的線程以外的所有託管線程均會掛起。

下圖演示了觸發垃圾回收並導致其他線程掛起的線程:

線程如何觸發垃圾回收的屏幕截圖。

非託管資源

對於應用程序創建的大多數對象,可以依賴垃圾回收自動執行必要的內存管理任務。 但是,非託管資源需要顯式清除。 最常用的非託管資源類型是包裝操作系統資源的對象,例如,文件句柄、窗口句柄或網絡連接。 雖然垃圾回收器可以跟蹤封裝非託管資源的託管對象的生存期,但卻無法具體瞭解如何清理資源。

定義封裝非託管資源的對象時,建議在公共 Dispose 方法中提供必要的代碼以清理非託管資源。 通過提供 Dispose 方法,對象的用戶可以在使用完對象後顯式釋放資源。 使用封裝非託管資源的對象時,務必要在需要時調用 Dispose

還必須提供一種釋放非託管資源的方法,以防類型使用者忘記調用 Dispose。 可以使用安全句柄來包裝非託管資源,也可以重寫 Object.Finalize() 方法。

有關清理非託管資源的詳細信息,請參閱清理非託管資源

 

自動內存管理

自動內存管理是公共語言運行時在託管執行過程中提供的服務之一。 公共語言運行時的垃圾回收器爲應用程序管理內存的分配和釋放。 對開發人員而言,這就意味着在開發託管應用程序時不必編寫執行內存管理任務的代碼。 自動內存管理可解決常見問題,例如,忘記釋放對象並導致內存泄漏,或嘗試訪問已釋放對象的內存。 本節描述垃圾回收器如何分配和釋放內存。

分配內存

初始化新進程時,運行時會爲進程保留一個連續的地址空間區域。 這個保留的地址空間被稱爲託管堆。 託管堆維護着一個指針,用它指向將在堆中分配的下一個對象的地址。 最初,該指針設置爲指向託管堆的基址。 託管堆上包含了所有引用類型。 應用程序創建第一個引用類型時,將爲託管堆的基址中的類型分配內存。 應用程序創建下一個對象時,垃圾回收器在緊接第一個對象後面的地址空間內爲它分配內存。 只要地址空間可用,垃圾回收器就會繼續以這種方式爲新對象分配空間。

從託管堆中分配內存要比非託管內存分配速度快。 由於運行時通過爲指針添加值來爲對象分配內存,所以這幾乎和從堆棧中分配內存一樣快。 另外,由於連續分配的新對象在託管堆中是連續存儲,所以應用程序可以快速訪問這些對象。

釋放內存

垃圾回收器的優化引擎根據所執行的分配決定執行回收的最佳時間。 垃圾回收器在執行回收時,會釋放應用程序不再使用的對象的內存。 它通過檢查應用程序的根來確定不再使用的對象。 每個應用程序都有一組根。 每個根或者引用託管堆中的對象,或者設置爲空。 應用程序的根包含線程堆棧上的靜態字段、局部變量和參數以及 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 級託管堆中,直到在將來的回收中確定它們爲無法訪問爲止。

爲非託管資源釋放內存

對於應用程序創建的大多數對象,可以依賴垃圾回收器自動執行必要的內存管理任務。 但是,非託管資源需要顯式清除。 最常用的非託管資源類型是包裝操作系統資源的對象,例如,文件句柄、窗口句柄或網絡連接。 雖然垃圾回收器可以跟蹤封裝非託管資源的託管對象的生存期,但卻無法具體瞭解如何清理資源。 創建封裝非託管資源的對象時,建議在公共 Dispose 方法中提供必要的代碼以清理非託管資源。 通過提供 Dispose 方法,對象的用戶可以在使用完對象後顯式釋放其內存。 使用封裝非託管資源的對象時,應該瞭解 Dispose 並在必要時調用它。 有關清理非託管資源的詳細信息和實現 Dispose 的設計模式示例,請參見垃圾回收

請參閱


 

Windows 系統上的大型對象堆

.NET 垃圾回收器 (GC) 將對象分爲小型和大型對象。 如果是大型對象,它的某些特性將比對象較小時顯得更爲重要。 例如,壓縮大型對象(也就是在內存中將其複製到堆上的其他地方)的費用相當高。 因此,垃圾回收器將大型對象放置在大型對象堆 (LOH) 上。 本文將討論符合什麼條件的對象才能稱之爲大型對象,如何回收大型對象,以及大型對象具備哪些性能意義。

 重要

本文僅討論 .NET Framework 中的大型對象堆和 Windows 系統上運行的 .NET Core。 不包括在其他平臺上的 .NET 實現上運行的 LOH。

對象如何在 LOH 上結束

如果對象的大小大於或等於 85,000 字節,將被視爲大型對象。 此數字根據性能優化確定。 對象分配請求爲 85,000 字節或更大時,運行時會將其分配到大型對象堆。

若要了解其意義,可查看垃圾回收器的部分相關基礎知識。

垃圾回收器是分代回收器。 它包含三代:第 0 代、第 1 代和第 2 代。 包含 3 代的原因是,在優化良好的應用中,大部分對象都在第 0 代就清除了。 例如,在服務器應用中,與每個請求相關的分配應在請求完成後清除。 仍存在的分配請求將轉到第 1 代,並在那裏進行清除。 從本質上講,第 1 代是新對象區域與生存期較長的對象區域之間的緩衝區。

新分配的對象構成新一代對象,並隱式地成爲第 0 代集合。 但是,如果它們是大型對象,它們將延續到大型對象堆 (LOH),這有時稱爲第 3 代。 第 3 代是在第 2 代中邏輯收集的物理生成。

大型對象屬於第 2 代,因爲只有在第 2 代回收期間才能回收它們。 回收一代時,同時也會回收它前面的所有代。 例如,執行第 1 代 GC 時,將同時回收第 1 代和第 0 代。 執行第 2 代 GC 時,將回收整個堆。 因此,第 2 代 GC 還可稱爲“完整 GC”。 本文引用第 2 代 GC 而不是完整 GC,但這兩個術語是可以互換的。

代可提供 GC 堆的邏輯視圖。 實際上,對象存在於託管堆段中。 託管堆段是 GC 通過調用 VirtualAlloc 功能代表託管代碼在操作系統上保留的內存塊。 加載 CLR 時,GC 分配兩個初始堆段:一個用於小型對象(小型對象堆或 SOH),一個用於大型對象(大型對象堆)。

然後,通過將託管對象置於這些託管堆段上來滿足分配請求。 如果該對象小於 85,000 字節,則將它置於 SOH 的段上,否則,將它置於 LOH 段。 隨着分配到各段上的對象越來越多,會以較小塊的形式提交這些段。 對於 SOH,GC 未處理的對象將提升爲下一代。 第 0 代回收未處理的對象現在視爲第 1 代對象,以此類推。 但是,最後一代回收未處理的對象仍會被視爲最後一代中的對象。 也就是說,第 2 代垃圾回收未處理的對象仍是第 2 代對象;LOH 未處理的對象仍是 LOH 對象(由第 2 代回收)。

用戶代碼只能在第 0 代(小型對象)或 LOH(大型對象)中分配。 只有 GC 可以在第 1 代(通過提升第 0 代回收未處理的對象)和第 2 代(通過提升第 1 代回收未處理的對象)中“分配”對象。

觸發垃圾回收後,GC 將尋找存在的對象並將它們壓縮。 但是由於壓縮費用很高,GC 會掃過 LOH,列出沒有被清除的對象列表以供以後重新使用,從而滿足大型對象的分配請求。 相鄰的被清除對象將組成一個自由對象。

.NET Core 和 .NET Framework(從 .NET Framework 4.5.1 開始)包括 GCSettings.LargeObjectHeapCompactionMode 屬性,該屬性可讓用戶指定在下一完整阻止 GC 期間壓縮 LOH。 並且在以後,.NET 可能會自動決定壓縮 LOH。 這就意味着,如果分配了大型對象並希望確保它們不被移動,則應將其固定起來。

圖 1 說明了一種情況,在第一次第 0 代 GC 後 GC 形成了第 1 代,其中 Obj1 和 Obj3 被清除;在第一次第 1 代 GC 後形成了第 2 代,其中 Obj2 和 Obj5 被清除。 請注意此圖和下圖僅用於說明,它們只包含能更好展示堆上的情況的極少幾個對象。 實際上,GC 中通常包含更多的對象。

圖 1:第 0 代 GC 和第 1 代 GC
圖 1:第 0 代和第 1 代 GC。

圖 2 顯示了第 2 代 GC 發現 Obj1 和 Obj2 被清除後,GC 在內存中形成了相鄰的可用空間,由 Obj1 和 Obj2 佔用,然後用於滿足 Obj4 的分配要求。 從最後一個對象 Obj3 到此段末尾的空間仍可用於滿足分配請求。

圖 2:第 2 代 GC 後
圖 2:第 2 代 GC 後

如果沒有足夠的可用空間來容納大型對象分配請求,GC 首先嚐試從操作系統獲取更多段。 如果失敗了,它將觸發第 2 代 GC,試圖釋放部分空間。

在第 1 代或第 2 代 GC 期間,垃圾回收器會通過調用 VirtualFree 功能將不包含活動對象的段釋放回操作系統。 將退回最後一個活動對象到段末尾的空間(第 0 代/第 1 代存在的短暫段上的空間除外,垃圾回收器會在該段上會保存部分提交內容,因爲應用程序將在其中立即分配)。 而且,儘管已重置可用空間,但仍會提交它們,這意味着操作系統無需將其中的數據重新寫入磁盤。

由於 LOH 僅在第 2 代 GC 期間進行回收,所以 LOH 段僅在此類 GC 期間可用。 圖 3 說明了一種情況,在此情況下,垃圾回收器將某段(段 2)釋放回操作系統並且退回剩餘段上更多的空間。 如果需要使用該段末尾的已退回空間來滿足大型對象分配請求,它會再次提交該內存。 (有關提交/退回的解釋說明,請參閱 VirtualAlloc 的文檔)。

圖 3:第 2 代 GC 後的 LOH
圖 3:第 2 代 GC 後的 LOH

何時收集大型對象?

通常情況下,出現以下三種情形中的任一情況,都會執行 GC:

  • 分配超出第 0 代或大型對象閾值。

    閾值是某代的屬性。 垃圾回收器在其中分配對象時,會爲代設置閾值。 超出閾值後,會在該代上觸發 GC。 因此,分配小型或大型對象時,需要分別使用第 0 代和 LOH 的閾值。 當垃圾回收器分配到第 1 代和第 2 代中時,將使用它們的閾值。 運行此程序時,會動態調整這些閾值。

    這是典型情況,大部分 GC 執行都因爲託管堆上的分配。

  • 調用 GC.Collect 方法。

    如果調用無參數 GC.Collect() 方法,或另一個重載作爲參數傳遞到 GC.MaxGeneration,將會一起收集 LOH 和剩餘的託管堆。

  • 系統處於內存不足的狀況。

    垃圾回收器收到來自操作系統 的高內存通知時,會發生以上情況。 如果垃圾回收器認爲執行第 2 代 GC 會有效率,它將觸發第 2 代。

LOH 性能意義

大型對象堆上的分配通過以下幾種方式影響性能。

  • 分配成本。

    CLR 確保清除了它提供的每個新對象的內存。 這意味着大型對象的分配成本由清理的內存(除非觸發了 GC)決定。 如果需要 2 輪才能清除一個字節,即需要 170,000 輪才能清除最小的大型對象。 清除 2GHz 計算機上 16MB 對象的內存大約需要 16ms。 這些成本相當大。

  • 回收成本。

    因爲 LOH 和第 2 代一起回收,如果超出了它們之中任何一個的閾值,則觸發第 2 代回收。 如果由於 LOH 觸發第 2 代回收,第 2 代沒有必要在 GC 後變得更小。 如果第 2 代上數據不多,則影響較小。 但是,如果第 2 代很大,則觸發多次第 2 代 GC 可能會產生性能問題。 如果很多大型對象都在短暫的基礎上進行分配,並且擁有大型 SOH,則可能會花費太多時間來執行 GC。 除此之外,如果連續分配並且釋放真正的大型對象,那麼分配成本可能會增加。

  • 具有引用類型的數組元素。

    LOH 上的特大型對象通常是數組(很少會有非常大的實例對象)。 如果數組的元素有豐富的引用,則可能產生成本;如果元素沒有豐富的引用,將不會產生此類成本。 如果元素不包含任何引用,則垃圾回收器根本無需處理此數組。 例如,如果使用數組存儲二進制樹中的節點,一種實現方法是按實際節點引用某個節點的左側節點和右側節點:

    C#
    class Node
    {
       Data d;
       Node left;
       Node right;
    };
    
    Node[] binary_tr = new Node [num_nodes];
    

    如果 num_nodes 非常大,則垃圾回收器需要處理每個元素的至少兩個引用。 另一種方法是存儲左側節點和右側節點的索引:

    C#
    class Node
    {
       Data d;
       uint left_index;
       uint right_index;
    } ;
    

    不要將左側節點的數據引用爲 left.d,而是將其引用爲 binary_tr[left_index].d。 而垃圾回收器無需查看左側節點和右側節點的任何引用。

在這三種因素中,前兩個通常比第三個更重要。 因此,建議分配重複使用的大型對象池,而不是分配臨時大型對象。

收集 LOH 的性能數據

收集特定區域的性能數據之前,應完成以下操作:

  1. 找到應查看此區域的證據。

  2. 排查你知道的其他區域,確保未發現可解釋上述性能問題的內容。

有關內存和 CPU 的基礎知識的詳細信息,請參閱博客嘗試找出解決方案之前先了解問題

可使用以下工具來收集 LOH 性能數據:

.NET CLR 內存性能計數器

這些性能計數器通常是調查性能問題的第一步(但是推薦使用 ETW 事件)。 通過添加所需計數器配置性能監視器,如圖 4 所示。 與 LOH 相關的是:

  • 第 2 代回收次數

    顯示自進程開始起第 2 代 GC 發生的次數。 此計數器在第 2 代回收結束時遞增(也稱爲完整垃圾回收)。 此計數器顯示上次觀測的值。

  • 大型對象堆大小

    以字節顯示當前大小,包括 LOH 的可用空間。 此計數器在垃圾回收結束時更新,不在每次分配時更新。

查看性能計數器的常用方法是使用性能監視器 (perfmon.exe)。 使用“添加計數器”可爲關注的進程添加感興趣的計數器。 可將性能計數器數據保存在日誌文件中,如圖 4 所示:

屏幕截圖顯示添加性能計數器。 圖 4:第 2 代 GC 後的 LOH

也可以編程方式查詢性能計數器。 大部分人在例行測試過程中都採用此方式進行收集。 如果發現計數器顯示的值不正常,則可以使用其他方法獲得更多詳細信息以幫助調查。

 備註

建議使用 ETW 事件代替性能計數,因爲 ETW 提供更豐富的信息。

ETW 事件

垃圾回收器提供豐富的 ETW 事件集,幫助瞭解堆的工作內容和工作原理。 以下博客文章演示瞭如何使用 ETW 收集和了解 GC 事件:

若要標識由臨時 LOH 分配造成的過多第 2 代 GC 次數,請查看 GC 的“觸發原因”列。 有關僅分配臨時大型對象的簡單測試,可使用以下 PerfView 命令行收集 ETW 事件的信息:

控制檯
perfview /GCCollectOnly /AcceptEULA /nogui collect

結果類似於以下類容:

屏幕截圖顯示 PerfView 中的 ETW 事件。 圖 5:使用 PerfView 顯示的 ETW 事件

如下所示,所有 GC 都是第 2 代 GC,並且都由 AllocLarge 觸發,這表示分配大型對象會觸發此 GC。 我們知道這些分配是臨時的,因爲“LOH 未清理率 %”列顯示爲 1%。

可以收集顯示分配這些大寫對象的人員的其他 ETW 事件。 以下命令行:

控制檯
perfview /GCOnly /AcceptEULA /nogui collect

收集 AllocationTick 事件,大約每 10 萬次分配就會觸發該事件。 換句話說,每次分配大型對象都會觸發事件。 然後可查看某個 GC 堆分配視圖,該視圖顯示分配大型對象的調用堆棧:

屏幕截圖顯示垃圾回收器堆視圖。 圖 6:GC 堆分配視圖

如圖所示,這是從 Main 方法分配大型對象的簡單測試。

調試器

如果只有內存轉儲,則需要查看 LOH 上實際有哪些對象,你可使用 .NET 提供的 SoS 調試器擴展來查看。

 備註

此部分提到的調試命令適用於 Windows 調試器

以下內容顯示了分析 LOH 的示例輸出:

控制檯
0:003> .loadby sos mscorwks
0:003> !eeheap -gc
Number of GC Heaps: 1
generation 0 starts at 0x013e35ec
sdgeneration 1 starts at 0x013e1b6c
generation 2 starts at 0x013e1000
ephemeral segment allocation context: none
segment   begin allocated     size
0018f2d0 790d5588 790f4b38 0x0001f5b0(128432)
013e0000 013e1000 013e35f8 0x000025f8(9720)
Large object heap starts at 0x023e1000
segment   begin allocated     size
023e0000 023e1000 033db630 0x00ffa630(16754224)
033e0000 033e1000 043cdf98 0x00fecf98(16699288)
043e0000 043e1000 05368b58 0x00f87b58(16284504)
Total Size 0x2f90cc8(49876168)
------------------------------
GC Heap Size 0x2f90cc8(49876168)
0:003> !dumpheap -stat 023e1000 033db630
total 133 objects
Statistics:
MT   Count   TotalSize Class Name
001521d0       66     2081792     Free
7912273c       63     6663696 System.Byte[]
7912254c       4     8008736 System.Object[]
Total 133 objects

LOH 堆大小爲 (16,754,224 + 16,699,288 + 16,284,504) = 49,738,016 字節。 在地址 023e1000 和地址 033db630 之間,8,008,736 字節由 System.Object 對象的數組佔用,6,663,696 字節由 System.Byte 對象的數組佔用,2,081,792 字節由可用空間佔用。

有時,調試器顯示 LOH 的總大小少於 85,000 個字節。 這是由於運行時本身使用 LOH 分配某些小於大型對象的對象引起的。

因爲不會壓縮 LOH,有時會懷疑 LOH 是碎片源。 碎片表示:

  • 託管堆的碎片由託管對象之間的可用空間量來表示。 在 SoS 中,!dumpheap –type Free 命令顯示託管對象之間的可用空間量。

  • 虛擬內存 (VM) 地址空間的碎片是標識爲 MEM_FREE 的內存。 可在 windbg 中使用各種調試器命令來獲取碎片。

    以下示例顯示 VM 空間中的碎片:

    控制檯
    0:000> !address
    00000000 : 00000000 - 00010000
    Type     00000000
    Protect 00000001 PAGE_NOACCESS
    State   00010000 MEM_FREE
    Usage   RegionUsageFree
    00010000 : 00010000 - 00002000
    Type     00020000 MEM_PRIVATE
    Protect 00000004 PAGE_READWRITE
    State   00001000 MEM_COMMIT
    Usage   RegionUsageEnvironmentBlock
    00012000 : 00012000 - 0000e000
    Type     00000000
    Protect 00000001 PAGE_NOACCESS
    State   00010000 MEM_FREE
    Usage   RegionUsageFree
    … [omitted]
    -------------------- Usage SUMMARY --------------------------
    TotSize (     KB)   Pct(Tots) Pct(Busy)   Usage
    701000 (   7172) : 00.34%   20.69%   : RegionUsageIsVAD
    7de15000 ( 2062420) : 98.35%   00.00%   : RegionUsageFree
    1452000 (   20808) : 00.99%   60.02%   : RegionUsageImage
    300000 (   3072) : 00.15%   08.86%   : RegionUsageStack
    3000 (     12) : 00.00%   00.03%   : RegionUsageTeb
    381000 (   3588) : 00.17%   10.35%   : RegionUsageHeap
    0 (       0) : 00.00%   00.00%   : RegionUsagePageHeap
    1000 (       4) : 00.00%   00.01%   : RegionUsagePeb
    1000 (       4) : 00.00%   00.01%   : RegionUsageProcessParametrs
    2000 (       8) : 00.00%   00.02%   : RegionUsageEnvironmentBlock
    Tot: 7fff0000 (2097088 KB) Busy: 021db000 (34668 KB)
    
    -------------------- Type SUMMARY --------------------------
    TotSize (     KB)   Pct(Tots) Usage
    7de15000 ( 2062420) : 98.35%   : <free>
    1452000 (   20808) : 00.99%   : MEM_IMAGE
    69f000 (   6780) : 00.32%   : MEM_MAPPED
    6ea000 (   7080) : 00.34%   : MEM_PRIVATE
    
    -------------------- State SUMMARY --------------------------
    TotSize (     KB)   Pct(Tots) Usage
    1a58000 (   26976) : 01.29%   : MEM_COMMIT
    7de15000 ( 2062420) : 98.35%   : MEM_FREE
    783000 (   7692) : 00.37%   : MEM_RESERVE
    
    Largest free region: Base 01432000 - Size 707ee000 (1843128 KB)
    

通常看到的更多是由臨時大型對象導致的 VM 碎片,這些對象要求垃圾回收器頻繁從操作系統獲取新的託管堆段,並將空託管堆段釋放回操作系統。

要驗證 LOH 是否會生成 VM 碎片,可在 VirtualAlloc 和 VirtualFree 上設置一個斷點,查看是誰調用了它們。 例如,如果想知道誰曾嘗試從操作系統分配大於 8 MB 的虛擬內存塊,可按以下方式設置斷點:

控制檯
bp kernel32!virtualalloc "j (dwo(@esp+8)>800000) 'kb';'g'"

只有在分配大小大於 8 MB (0x800000) 的情況下調用 VirtualAlloc 時,此命令纔會進入調試器並顯示調用堆棧。

CLR 2.0 增加了稱爲“VM 囤積”的功能,用於頻繁獲取和釋放段(包括在大型和小型對象堆上)的情況。 若要指定 VM 囤積,可通過託管 API 指定稱爲 STARTUP_HOARD_GC_VM 的啓動標記。 CLR 退回這些段上的內存並將其添加到備用列表中,而不會將該空段釋放回操作系統。 (請注意 CLR 不會針對太大型的段執行此操作。)CLR 稍後將使用這些段來滿足新段請求。 下一次應用需要新段時,CLR 將使用此備用列表中的某個足夠大的段。

VM 囤積還可用於想要保存已獲取段的應用程序(例如屬於系統上運行的主要應用的部分服務器應用),以避免內存不足的異常。

強烈建議你在使用此功能時認真測試應用程序,以確保應用程序的內存使用情況比較穩定。

 

垃圾回收和性能

本文介紹與垃圾回收和內存使用情況相關的問題。 它解決了關於託管堆的問題,並解釋瞭如何最小化垃圾回收對應用程序的影響。 每個問題具有訪問可用來調查問題的過程的鏈接。

性能分析工具

以下各節介紹了可用於調查內存使用情況和垃圾回收問題的工具。 本文中稍後提供的過程將引用這些工具。

內存性能計數器

可以使用性能計數器來收集性能數據。 有關說明,請參閱運行時分析。 如 .NET 中的性能計數器中所述,性能計數器的 .NET CLR 內存類別提供有關垃圾回收器的信息。

用 SOS 調試

可以使用 Windows 調試器 (WinDbg) 檢查託管堆上的對象。

若要安裝 WinDbg,請從下載 Windows 調試工具頁安裝 Windows 調試工具。

垃圾回收 ETW 事件

Windows 事件跟蹤 (ETW) 是一個跟蹤系統,對由 .NET 提供的分析和調試支持提供補充。 從 .NET Framework 4 開始,垃圾回收 ETW 事件將捕獲有用信息,用於從統計的角度來分析託管堆。 例如,在將要發生垃圾回收時引發的 GCStart_V1 事件提供了以下信息:

  • 正在收集哪一代對象。
  • 是什麼觸發了垃圾回收。
  • 垃圾回收的類型(併發或非併發)。

ETW 事件日誌有效,且不會掩蓋與垃圾回收相關的任何性能問題。 一個進程可以通過結合 ETW 事件來提供其自身的事件。 登錄後,可以關聯應用程序事件和垃圾回收事件,以確定如何以及何時出現堆問題。 例如,服務器應用程序可以在客戶端請求開始和結束時提供事件。

分析 API

公共語言運行時 (CLR) 分析接口將提供有關垃圾回收期間受影響對象的詳細信息。 垃圾回收開始和結束時,可以通知探查器。 它可以提供有關託管堆上對象的報告,其中包括每一代對象的標識。 有關詳細信息,請參閱分析概述

探查器可以提供全面的信息。 但是,複雜的探查器可能會修改應用程序的行爲。

應用程序域資源監控

從 .NET Framework 4 開始,應用程序域資源監視 (ARM) 使主機可以通過應用程序域監視 CPU 和內存使用情況。 有關詳細信息,請參閱應用程序域資源監控

排查性能問題

第一步是確定問題是否確實爲垃圾回收。 如果確定是,則從以下列表進行選擇,以解決該問題。

問題:拋出內存不足異常

對於引發的託管 OutOfMemoryException,存在以下兩種合理的情況:

  • 虛擬內存不足。

    垃圾回收器按預先確定大小的分段來分配系統內存。 如果分配需要其他段,但在進程的虛擬內存空間中沒有剩餘的連續可用塊了,則託管堆的分配將失敗。

  • 沒有足夠的物理內存來分配。

如果確定異常不合法,請使用以下信息與 Microsoft 客戶服務和支持聯繫:

  • 帶有託管內存不足異常的堆棧。
  • 完整內存轉儲。
  • 證明這不是合法內存不足異常的數據包括顯示虛擬或物理內存不是問題的數據。

問題:進程佔用過多內存

通常會假設 Windows 任務管理器“性能”選項卡上的內存使用量顯示可以指示何時使用了太多內存。 然而,該顯示與工作集相關;它不提供有關虛擬內存使用量的信息。

如果確定問題是託管堆引發的,必須測量一段時間的託管堆,以確定模式。

如果確定問題不是託管堆引發的,則必須使用本地調試。

問題:垃圾回收器回收對象的速度不夠快

當出現對象好像未按垃圾回收的預期進行回收的情況時,必須確定是否存在任何對這些對象的強引用。

如果沒有對包含死對象的一代進行垃圾回收,這表示尚未運行死對象的終結器,你也可能會遇到以上問題。 例如,當正在運行一個單線程單元 (STA) 應用程序並且服務終結器隊列的線程不能調用至其中時,可能發生這種問題。

問題:託管堆太零碎

碎片級別將計算爲可用空間佔這一代已分配的總內存的比率。 對於第 2 代,可接受的碎片級別不能超過 20%。 因爲第 2 代可以變得很大,所以碎片的比率比絕對值更重要。

第 0 代中存在大量可用空間,這不是問題,因爲新的對象將在其中進行分配。

碎片始終出現在大型對象堆中,因爲它沒有進行壓縮。 相鄰的可用對象會自然地摺疊至一個單個的空間,以滿足大型對象的分配請求。

在第 1 代和第 2 代中,碎片可能會成爲問題。 如果它們在垃圾回收後還有大量的可用空間,則應用程序對象的使用可能需要進行修改,並且應考慮重新評估長期對象的生存期。

固定對象過多可能會增加碎片。 如果碎片太多,則可以固定許多對象。

如果虛擬內存的碎片阻止垃圾回收器添加段,原因可能是下列之一:

  • 頻繁加載和卸載許多小的程序集。

  • 與非託管代碼互操作時,保留了太多對 COM 對象的引用。

  • 大型暫時性對象的創建會導致大型對象堆頻繁分配和釋放堆段。

    當承載 CLR 時,應用程序可以請求垃圾回收器保留其片段。 這將減少段分配的頻率。 通過使用 STARTUP_FLAGS 枚舉中的 STARTUP_HOARD_GC_VM 標誌來完成。

如果認爲沒有出現碎片的合理原因,請與 Microsoft 客戶服務和支持聯繫。

問題:垃圾回收暫停時間太長

由於垃圾回收軟實時操作,因此應用程序必須能夠容忍暫停。 軟實時的一個衡量標準是 95% 的操作必須按時完成。

在併發垃圾回收中,允許託管線程在一個回收過程中運行,這意味着暫停時間會非常短。

短暫的垃圾回收(第 0 代和第 1 代)只會持續幾毫秒,所以減少暫停時間通常是不可行的。 然而,你可以通過更改應用程序的分配請求的模式,在第 2 代回收中減少暫停。

另一個更準確的方法是使用垃圾回收 ETW 事件。 可以通過爲某個事件序列添加時間戳的差異來查找回收的計時。 整個集合序列包括暫停執行引擎、垃圾回收本身以及恢復執行引擎。

可以使用垃圾回收通知,確定服務器是否將要進行第 2 代回收,以及將請求重新路由到另一個服務器是否可以減輕任何暫停問題。

問題:第 0 代太大

第 0 代可能在 64 位系統上有更多的對象,尤其是當使用服務器垃圾回收而不是工作站垃圾回收時。 這是因爲觸發 0 代垃圾回收的閾值在這些環境中更高,且 0 代回收可以變得更大。 觸發垃圾回收之前,當應用程序分配更多的內存時,性能將會提高。

問題:垃圾回收期間的 CPU 使用率太高

在垃圾回收期間,CPU 的使用率會很高。 如果在垃圾回收中花費大量的處理時間,則回收的數量將過於頻繁或回收的持續時間將過長。 託管堆上增加的對象分配率將導致垃圾回收更頻繁地發生。 減少分配速率可減少垃圾回收的頻率。

可以通過使用 Allocated Bytes/second 性能計數器來監視分配速率。 有關更多信息,請參閱 .NET 中的性能計數器

收集的持續時間是分配後倖存對象數量的主要因素。 如果有許多對象仍需收集,則垃圾回收器必須要檢查大量的內存。 壓縮倖存對象的工作很耗時。 若要確定回收期間處理對象的數量,請在指定代的垃圾回收結束時,在調試器中設置一個斷點。

故障排除指南

本部分介紹在開始調查時應考慮的準則。

工作站或服務器垃圾回收

確定是否正在使用正確的垃圾回收類型。 如果應用程序使用多個線程和對象實例,則使用服務器垃圾回收,而不是工作站垃圾回收。 服務器垃圾回收在多個線程上進行操作,而工作站垃圾回收則需要應用程序的多個實例運行它們自己的垃圾回收線程並爭取 CPU 時間。

低負載且不常在後臺(如服務)執行任務的應用程序,可以在禁用併發垃圾回收的情況下使用工作站垃圾回收。

何時衡量託管堆的大小

除非使用探查器,否則必須建立一致的測量模式,以有效地診斷性能問題。 若要建立一個計劃,請考慮以下幾點:

  • 如果在第 2 代垃圾回收後測量,則整個託管的堆將不再存在垃圾(死對象)。
  • 如果在一個 0 代垃圾回收後立即進行測量,則尚不會收集第 1 代和 2 中的對象。
  • 如果在垃圾回收之前立即進行測量,則你將在垃圾回收啓動之前,測量儘可能多的分配。
  • 在垃圾回收期間進行測量會出現問題,因爲垃圾回收器的數據結構對於遍歷是無效狀態,可能不能提供完整的結果。 這是設計使然。
  • 當與併發垃圾回收一起使用工作站垃圾回收時,回收的對象不會進行壓縮,因此,堆的大小可能會相同或更大(碎片可以使它看起來更大)。
  • 第 2 代上的併發垃圾回收在物理內存加載過高時,將被延遲。

以下過程介紹如何設置一個斷點,以便測量託管堆。

若要在垃圾回收結束時設置一個斷點

  • 在加載了 SOS 調試器擴展的 WinDbg 中,輸入以下命令:

    bp mscorwks!WKS::GCHeap::RestartEE "j (dwo(mscorwks!WKS::GCHeap::GcCondemnedGeneration)==2) 'kb';'g'"

    將 GcCondemnedGeneration 設置爲所需的代。 此命令要求私有符號。

    如果在已回收第 2 代對象以進行垃圾回收後執行 RestartEE,則此命令會強制中斷。

    在服務器垃圾回收中,只有一個線程會調用 RestartEE,因此在第 2 代垃圾回收期間,此斷點只會出現一次。

性能檢查過程

本部分將介紹下列過程,以避免造成性能問題的原因:

若要確定問題是否是垃圾回收引起

  • 請檢查以下兩個內存性能計數器:

    • GC 所佔時間百分比。 顯示執行最後一個垃圾回收週期後,執行垃圾回收所用運行時間的百分比。 使用此計數器確定垃圾回收器是否花費太多時間來使託管堆空間可用。 如果垃圾回收所用的時間相對較短,這可能表示託管堆之外存在資源問題。 當涉及併發或後臺垃圾回收時,此計數器可能不準確。

    • 已提交的字節總數。 顯示垃圾回收器當前已提交的虛擬內存量。 使用此計數器確定垃圾回收器所佔用的內存是否是應用程序所使用的內存的過多部分。

    大多數的內存性能計數器會在每次垃圾回收結束時進行更新。 因此,它們可能不會反映你希望瞭解的當前情況。

若要確定是否已託管內存不足異常

  1. 在加載了 SOS 調試器擴展的 WinDbg 或 Visual Studio 調試器中,輸入打印異常 (pe) 命令:

    !pe

    如果已託管異常,OutOfMemoryException 將顯示爲異常類型,如以下示例中所示。

    控制檯
    Exception object: 39594518
    Exception type: System.OutOfMemoryException
    Message: <none>
    InnerException: <none>
    StackTrace (generated):
    
  2. 如果輸出沒有指定異常,則必須確定內存不足異常來自哪個線程。 在調試器中輸入以下命令,以顯示所有帶調用堆棧的線程:

    ~\*kb

    具有存在異常調用的堆棧的線程會由 RaiseTheException 參數進行指示。 這是託管異常對象。

    控制檯
    28adfb44 7923918f 5b61f2b4 00000000 5b61f2b4 mscorwks!RaiseTheException+0xa0
    
  3. 可以使用以下命令來轉儲嵌套的異常。

    !pe -nested

    如果找不到任何異常,則非託管代碼將產生內存不足異常。

若要確定可保留的虛擬內存量

  • 在加載了 SOS 調試器擴展的 WinDbg 中輸入以下命令,以獲取最大的可用區域:

    !address -summary

    最大可用區域將如以下輸出所示進行顯示。

    控制檯
    Largest free region: Base 54000000 - Size 0003A980
    

    在此示例中,最大可用區域的大小大約爲 24000 KB(按十六進制形式則爲 3A980)。 此區域比垃圾回收器對分段所需的大小要小得多。

  • 使用 vmstat 命令:

    !vmstat

    最大可用區域是 MAXIMUM 列中的最大值,如以下輸出所示。

    控制檯
    TYPE        MINIMUM   MAXIMUM     AVERAGE   BLK COUNT   TOTAL
    ~~~~        ~~~~~~~   ~~~~~~~     ~~~~~~~   ~~~~~~~~~~  ~~~~
    Free:
    Small       8K        64K         46K       36          1,671K
    Medium      80K       864K        349K      3           1,047K
    Large       1,384K    1,278,848K  151,834K  12          1,822,015K
    Summary     8K        1,278,848K  35,779K   51          1,824,735K
    

若要確定是否有足夠的物理內存

  1. 則啓動 Windows 任務管理器。

  2. 在 Performance 選項卡上,查看已提交的值。 (在 Windows 7 中,查看 System group 中的 Commit (KB)。)

    如果 Total 接近於 Limit,則物理內存不足。

若要確定託管堆的內存提交量

  • 使用 # Total committed bytes 內存性能計數器獲取託管堆提交的字節數。 垃圾回收器根據需要在某個段上提交區塊,但不會全部在同一時間進行。

     備註

    請不要使用 # Bytes in all Heaps 性能計數器,因爲它不表示託管堆的實際內存使用情況。 代的大小包括在此值中,且實際上是其閾值大小,即如果代以對象進行填充,將引發垃圾回收的大小。 因此,此值通常爲零。

若要確定託管堆的內存保留量

  • 使用 # Total reserved bytes內存性能計數器。

    垃圾回收器按段保留內存,並可以通過使用 eeheap 命令確定一個段的開始位置。

     重要

    儘管可以確定垃圾回收器爲每個段分配的內存量,但是段的大小是特定於實現的,並可能會在任何時間(包括在定期更新中)進行更改。 應用程序不應假設特定段的大小或依賴於此大小,也不應嘗試配置段分配可用的內存量。

  • 在加載了 SOS 調試器擴展的 WinDbg 或 Visual Studio 調試器中,輸入以下命令:

    !eeheap -gc

    結果如下所示:

    控制檯
    Number of GC Heaps: 2
    ------------------------------
    Heap 0 (002db550)
    generation 0 starts at 0x02abe29c
    generation 1 starts at 0x02abdd08
    generation 2 starts at 0x02ab0038
    ephemeral segment allocation context: none
      segment    begin allocated     size
    02ab0000 02ab0038  02aceff4 0x0001efbc(126908)
    Large object heap starts at 0x0aab0038
      segment    begin allocated     size
    0aab0000 0aab0038  0aab2278 0x00002240(8768)
    Heap Size   0x211fc(135676)
    ------------------------------
    Heap 1 (002dc958)
    generation 0 starts at 0x06ab1bd8
    generation 1 starts at 0x06ab1bcc
    generation 2 starts at 0x06ab0038
    ephemeral segment allocation context: none
      segment    begin allocated     size
    06ab0000 06ab0038  06ab3be4 0x00003bac(15276)
    Large object heap starts at 0x0cab0038
      segment    begin allocated     size
    0cab0000 0cab0038  0cab0048 0x00000010(16)
    Heap Size    0x3bbc(15292)
    ------------------------------
    GC Heap Size   0x24db8(150968)
    

    由“段”指示的地址是段的起始地址。

若要確定第 2 代中的大型對象

  • 在加載了 SOS 調試器擴展的 WinDbg 或 Visual Studio 調試器中,輸入以下命令:

    !dumpheap –stat

    如果託管堆很大,則 dumpheap 可能需要一段時間才能完成。

    你可以從輸出的最後幾行開始分析,因爲它們列出了佔用了大多數空間的對象。 例如:

    控制檯
    2c6108d4   173712     14591808 DevExpress.XtraGrid.Views.Grid.ViewInfo.GridCellInfo
    00155f80      533     15216804      Free
    7a747c78   791070     15821400 System.Collections.Specialized.ListDictionary+DictionaryNode
    7a747bac   700930     19626040 System.Collections.Specialized.ListDictionary
    2c64e36c    78644     20762016 DevExpress.XtraEditors.ViewInfo.TextEditViewInfo
    79124228   121143     29064120 System.Object[]
    035f0ee4    81626     35588936 Toolkit.TlkOrder
    00fcae40     6193     44911636 WaveBasedStrategy.Tick_Snap[]
    791242ec    40182     90664128 System.Collections.Hashtable+bucket[]
    790fa3e0  3154024    137881448 System.String
    Total 8454945 objects
    

    所列出的最後一個對象是一個字符串,且佔用的空間最多。 可以檢查應用程序,以查看如何優化字符串對象。 若要查看 150 到 200 個字節之間的字符串,請輸入以下命令:

    !dumpheap -type System.String -min 150 -max 200

    如下所示是結果的一個示例。

    控制檯
    Address  MT           Size  Gen
    1875d2c0 790fa3e0      152    2 System.String HighlightNullStyle_Blotter_PendingOrder-11_Blotter_PendingOrder-11
    …
    

    對 ID 使用整數而非字符串,這樣可能會更有效。 如果數千次重複相同的字符串,請考慮字符串暫留。 有關字符串暫留的詳細信息,請參閱 String.Intern 方法的參考主題。

若要確定對對象的引用

  • 在加載了 SOS 調試器擴展的 WinDbg 中,輸入以下命令,以列出對對象的引用:

    !gcroot

  • 若要確定對特定對象的引用,包括地址:

    !gcroot 1c37b2ac

    在堆棧上找到的根可能是誤報。 有關詳細信息,請參閱命令 !help gcroot

    控制檯
    ebx:Root:19011c5c(System.Windows.Forms.Application+ThreadContext)->
    19010b78(DemoApp.FormDemoApp)->
    19011158(System.Windows.Forms.PropertyStore)->
    … [omitted]
    1c3745ec(System.Data.DataTable)->
    1c3747a8(System.Data.DataColumnCollection)->
    1c3747f8(System.Collections.Hashtable)->
    1c376590(System.Collections.Hashtable+bucket[])->
    1c376c98(System.Data.DataColumn)->
    1c37b270(System.Data.Common.DoubleStorage)->
    1c37b2ac(System.Double[])
    Scan Thread 0 OSTHread 99c
    Scan Thread 6 OSTHread 484
    

    gcroot 命令可能需要很長時間才能完成。 任何不通過垃圾回收進行回收的對象是活動對象。 這意味着,某些根是直接或間接地保留於該對象,因此 gcroot 應將路徑信息返回到該對象。 應檢查返回的關係圖,並查看仍然引用這些對象的原因。

若要確定是否已運行終結器

  • 則運行包含以下代碼的測試程序:

    C#
    GC.Collect();
    GC.WaitForPendingFinalizers();
    GC.Collect();
    

    如果測試解決了此問題,這意味着垃圾回收器未回收對象,因爲這些對象的終結器已被掛起。 GC.WaitForPendingFinalizers 方法將啓用這些終結器來完成其任務,並解決問題。

若要確定是否存在等待被終結的對象

  1. 在加載了 SOS 調試器擴展的 WinDbg 或 Visual Studio 調試器中,輸入以下命令:

    !finalizequeue

    查看已準備好進行終結的對象的數目。 如果數目很多,則必須檢查這些終結器完全沒有進展或進展速度不夠快的原因。

  2. 若要獲取線程的輸出,請輸入以下命令:

    !threads -special

    此命令提供如下所示的輸出。

    控制檯
       OSID     Special thread type
    2    cd0    DbgHelper
    3    c18    Finalizer
    4    df0    GC SuspendEE
    

    終結器線程將指示當前正在運行的終結器(如果存在)。 當終結器線程沒有運行任何終結器時,則它正在等待一個事件告訴它進行工作。 大多數情況下,你將看到此狀態中的終結器線程,因爲它在 THREAD_HIGHEST_PRIORITY 處運行,並應快速完成運行終結器(如果存在)。

若要確定託管堆中的可用空間量

  • 在加載了 SOS 調試器擴展的 WinDbg 或 Visual Studio 調試器中,輸入以下命令:

    !dumpheap -type Free -stat

    此命令將顯示託管堆上所有可用對象的總大小,如以下示例中所示。

    控制檯
    total 230 objects
    Statistics:
          MT    Count    TotalSize Class Name
    00152b18      230     40958584      Free
    Total 230 objects
    
  • 若要確定第 0 代中的可用空間,請輸入以下命令以獲取代的內存使用信息:

    !eeheap -gc

    該命令將顯示類似以下所示的輸出。 最後一行將顯示暫時段。

    控制檯
    Heap 0 (0015ad08)
    generation 0 starts at 0x49521f8c
    generation 1 starts at 0x494d7f64
    generation 2 starts at 0x007f0038
    ephemeral segment allocation context: none
    segment  begin     allocated  size
    00178250 7a80d84c  7a82f1cc   0x00021980(137600)
    00161918 78c50e40  78c7056c   0x0001f72c(128812)
    007f0000 007f0038  047eed28   0x03ffecf0(67103984)
    3a120000 3a120038  3a3e84f8   0x002c84c0(2917568)
    46120000 46120038  49e05d04   0x03ce5ccc(63855820)
    
  • 計算 0 代使用的空間:

    ? 49e05d04-0x49521f8c

    結果如下所示: 0 代大約爲 9 MB。

    控制檯
    Evaluate expression: 9321848 = 008e3d78
    
  • 以下命令將轉儲 0 代範圍內的可用空間:

    !dumpheap -type Free -stat 0x49521f8c 49e05d04

    結果如下所示:

    控制檯
    ------------------------------
    Heap 0
    total 409 objects
    ------------------------------
    Heap 1
    total 0 objects
    ------------------------------
    Heap 2
    total 0 objects
    ------------------------------
    Heap 3
    total 0 objects
    ------------------------------
    total 409 objects
    Statistics:
          MT    Count TotalSize Class Name
    0015a498      409   7296540      Free
    Total 409 objects
    

    此輸出顯示堆的 0 代部分正在對對象使用 9 MB 的空間並且有 7 MB 可用。 此分析顯示了 0 代對碎片的貢獻程度。 此堆的使用量應從總量中扣除,作爲長期對象所產生的碎片的原因。

若要確定固定對象的數目

  • 在加載了 SOS 調試器擴展的 WinDbg 或 Visual Studio 調試器中,輸入以下命令:

    !gchandles

    顯示的統計信息包括固定句柄的數量,如以下示例所示。

    控制檯
    GC Handle Statistics:
    Strong Handles:      29
    Pinned Handles:      10
    

若要確定垃圾回收中的時間

  • 檢查 % Time in GC 內存性能計數器。

    通過使用採樣間隔時間來計算值。 因爲該計數器在每次垃圾回收結束時進行更新,所以如果在間隔期間沒有產生任何回收,則當前的示例將具有與之前的示例相同的值。

    回收時間是通過將採樣間隔時間乘以百分比值獲取的。

    以下數據顯示了爲時 8 秒的研究的 4 個採樣,彼此間隔 2 秒。 Gen0Gen1 和 Gen2 列顯示截至間隔結束時爲止已爲該代完成的垃圾回收總數。

    控制檯
    Interval    Gen0    Gen1    Gen2    % Time in GC
            1       9       3       1              10
            2      10       3       1               1
            3      11       3       1               3
            4      11       3       1               3
    

    當垃圾回收發生時,將不會顯示此信息,但可以確定時間間隔中發生的垃圾回收數。 假設出現最壞的情況,第 10 個 0 代垃圾回收在第 2 個間隔開始時完成,且第 11 個 0 代垃圾回收在第 3 個間隔結束時完成。 第 10 個和第 11 個垃圾回收結束時之間的時間約爲 2 秒鐘,並且性能計數器顯示爲 3%,因此第 11 個 0 代垃圾回收的持續時間爲(2 秒 * 3%= 60 毫秒)。

    在下一個示例中,有五個間隔。

    控制檯
    Interval    Gen0    Gen1    Gen2     % Time in GC
            1       9       3       1                3
            2      10       3       1                1
            3      11       4       1                1
            4      11       4       1                1
            5      11       4       2               20
    

    第 2 個第 2代垃圾回收在第 4 個間隔期間開始並在第 5 個間隔處完成。 假設最壞情況下,最後一次垃圾回收是針對在第 3 個間隔開始時完成的 0 代回收,且第 2 代垃圾回收在第 5 個間隔結束時完成。 因此,第 0 代垃圾回收結束和第 2 代垃圾回收結束之間的時間是 4 秒。 因爲 % Time in GC 計數器爲 20%,所以第 2 代垃圾回收可能使用的最長時間爲(4 秒 * 20%= 800 毫秒)。

  • 或者,可以通過使用垃圾回收 ETW 事件,確定垃圾回收的時長,並分析此信息以確定垃圾回收的持續時間。

    例如,以下數據顯示了一個發生在非併發垃圾回收期間的事件序列。

    控制檯
    Timestamp    Event name
    513052        GCSuspendEEBegin_V1
    513078        GCSuspendEEEnd
    513090        GCStart_V1
    517890        GCEnd_V1
    517894        GCHeapStats
    517897        GCRestartEEBegin
    517918        GCRestartEEEnd
    

    掛起託管線程花費了 26us (GCSuspendEEEnd – GCSuspendEEBegin_V1)。

    實際的垃圾回收花費了 4.8 毫秒 (GCEnd_V1 – GCStart_V1)。

    回覆執行託管線程花費了 21us (GCRestartEEEnd – GCRestartEEBegin)。

    以下輸出爲後臺垃圾回收提供了一個示例,幷包括進程、線程和事件字段。 (沒有顯示所有數據。)

    控制檯
    timestamp(us)    event name            process    thread    event field
    42504385        GCSuspendEEBegin_V1    Test.exe    4372             1
    42504648        GCSuspendEEEnd         Test.exe    4372
    42504816        GCStart_V1             Test.exe    4372        102019
    42504907        GCStart_V1             Test.exe    4372        102020
    42514170        GCEnd_V1               Test.exe    4372
    42514204        GCHeapStats            Test.exe    4372        102020
    42832052        GCRestartEEBegin       Test.exe    4372
    42832136        GCRestartEEEnd         Test.exe    4372
    63685394        GCSuspendEEBegin_V1    Test.exe    4744             6
    63686347        GCSuspendEEEnd         Test.exe    4744
    63784294        GCRestartEEBegin       Test.exe    4744
    63784407        GCRestartEEEnd         Test.exe    4744
    89931423        GCEnd_V1               Test.exe    4372        102019
    89931464        GCHeapStats            Test.exe    4372
    

    42504816 處的 GCStart_V1 事件指示此爲一個後臺垃圾回收,因爲最後一個字段是 1。 這將變爲垃圾回收 No.102019。

    將發生 GCStart 事件,因爲在開始後臺垃圾回收之前,需要一個暫時垃圾回收。 這將變爲垃圾回收 No. 102020。

    在 42514170 處,垃圾回收 No.102020 結束。 此時,將重新啓動託管線程。 這將在觸發此後臺垃圾回收的線程 4372 上完成。

    在線程 4744 上,發生了一個掛起。 這是唯一一次後臺垃圾回收不得不掛起託管線程。 此持續時間爲大約 99 毫秒 ((63784407-63685394)/1000)。

    後臺垃圾回收的 GCEnd 事件位於 89931423。 這意味着後臺垃圾回收持續了大約 47 秒 ((89931423-42504816)/1000)。

    託管線程運行時,可以查看發生的任意數量的暫時垃圾回收。

若要確定觸發垃圾回收的原因

  • 在加載了 SOS 調試器擴展的 WinDbg 或 Visual Studio 調試器中,輸入以下命令,以顯示所有帶調用堆棧的線程:

    ~*kb

    該命令將顯示類似以下所示的輸出。

    控制檯
    0012f3b0 79ff0bf8 mscorwks!WKS::GCHeap::GarbageCollect
    0012f454 30002894 mscorwks!GCInterface::CollectGeneration+0xa4
    0012f490 79fa22bd fragment_ni!request.Main(System.String[])+0x48
    

    如果垃圾回收是操作系統的內存不足通知引起的,則調用堆棧會非常相似,除了線程是終結器線程之外。 終結器線程將獲取異步內存不足的通知,並引發垃圾回收。

    如果垃圾回收是內存分配引起的,則堆棧顯示如下:

    控制檯
    0012f230 7a07c551 mscorwks!WKS::GCHeap::GarbageCollectGeneration
    0012f2b8 7a07cba8 mscorwks!WKS::gc_heap::try_allocate_more_space+0x1a1
    0012f2d4 7a07cefb mscorwks!WKS::gc_heap::allocate_more_space+0x18
    0012f2f4 7a02a51b mscorwks!WKS::GCHeap::Alloc+0x4b
    0012f310 7a02ae4c mscorwks!Alloc+0x60
    0012f364 7a030e46 mscorwks!FastAllocatePrimitiveArray+0xbd
    0012f424 300027f4 mscorwks!JIT_NewArr1+0x148
    000af70f 3000299f fragment_ni!request..ctor(Int32, Single)+0x20c
    0000002a 79fa22bd fragment_ni!request.Main(System.String[])+0x153
    

    實時幫助程序 (JIT_New*) 最終調用 GCHeap::GarbageCollectGeneration。 如果確定第 2 代垃圾回收是分配引起的,則必須確定第 2 代垃圾回收所分配的對象以及如何避免它們。 也就是說,想要確定第 2 代垃圾回收的開始和結束之間的差異,以及引發第 2 代回收的對象。

    例如,在調試器中輸入以下命令,以顯示第 2 代回收的開始:

    !dumpheap –stat

    輸出示例(經過刪減以顯示使用的最多空間的對象):

    控制檯
    79124228    31857      9862328 System.Object[]
    035f0384    25668     11601936 Toolkit.TlkPosition
    00155f80    21248     12256296      Free
    79103b6c   297003     13068132 System.Threading.ReaderWriterLock
    7a747ad4   708732     14174640 System.Collections.Specialized.HybridDictionary
    7a747c78   786498     15729960 System.Collections.Specialized.ListDictionary+DictionaryNode
    7a747bac   700298     19608344 System.Collections.Specialized.ListDictionary
    035f0ee4    89192     38887712 Toolkit.TlkOrder
    00fcae40     6193     44911636 WaveBasedStrategy.Tick_Snap[]
    7912c444    91616     71887080 System.Double[]
    791242ec    32451     82462728 System.Collections.Hashtable+bucket[]
    790fa3e0  2459154    112128436 System.String
    Total 6471774 objects
    

    在第 2 代結束時,重複該命令:

    !dumpheap –stat

    輸出示例(經過刪減以顯示使用的最多空間的對象):

    控制檯
    79124228    26648      9314256 System.Object[]
    035f0384    25668     11601936 Toolkit.TlkPosition
    79103b6c   296770     13057880 System.Threading.ReaderWriterLock
    7a747ad4   708730     14174600 System.Collections.Specialized.HybridDictionary
    7a747c78   786497     15729940 System.Collections.Specialized.ListDictionary+DictionaryNode
    7a747bac   700298     19608344 System.Collections.Specialized.ListDictionary
    00155f80    13806     34007212      Free
    035f0ee4    89187     38885532 Toolkit.TlkOrder
    00fcae40     6193     44911636 WaveBasedStrategy.Tick_Snap[]
    791242ec    32370     82359768 System.Collections.Hashtable+bucket[]
    790fa3e0  2440020    111341808 System.String
    Total 6417525 objects
    

    double[] 對象從輸出的末尾消失,這意味着它們被回收了。 這些對象大約佔 70 MB。 剩餘的對象沒有太多變化。 因此,這些 double[] 對象是第 2 代垃圾回收發生的原因。 下一步是確定 double[] 對象存在以及他們最後死亡的原因。 可以詢問代碼開發人員這些對象的來源,或使用 gcroot 命令。

若要確定 CPU 的使用率高是否是垃圾回收引起的

  • 將 % Time in GC 內存性能計數器的值與處理時間相關聯。

    如果 % Time in GC 值在與處理時間同時達到峯值,則垃圾回收將造成 CPU 使用率過高。 否則,配置應用程序,以查找發生使用率過高的位置。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章