預覽
Title | 描述 |
---|---|
垃圾回收的基本知識 | 描述垃圾回收的工作原理、如何在托管堆上分配對象,以及其他核心概念。 |
工作站和服務器垃圾回收 | 描述了客戶端應用的工作站垃圾回收與服務器應用的服務器垃圾回收之間的區別。 |
后台垃圾回收 | 描述了后台垃圾回收,它是在進行第二代回收時對第 0 代和第 1 代對象的回收。 |
大型對象堆 | 描述了大型對象堆 (LOH) 及其垃圾回收方式。 |
垃圾回收和性能 | 介紹了可用來診斷垃圾回收和性能問題的性能檢查。 |
已引發回收 | 描述如何完成垃圾回收。 |
延遲模式 | 描述確定垃圾回收侵入性的模式。 |
針對共享 Web 承載優化 | 介紹了如何在多個小網站共用的服務器上優化垃圾回收。 |
垃圾回收通知 | 介紹了如何確定全面垃圾回收的開始時間和結束時間。 |
應用程序域資源監視 | 介紹了如何監視應用程序域的 CPU 和內存使用情況。 |
弱引用 | 描述允許應用程序訪問對象的同時也允許垃圾回收器收集相應對象的功能。 |
0.特點
C# GC是指C#語言中的垃圾回收機制,它是一種自動管理內存的機制,它可以釋放不再使用的對象所占用的空間,從而避免內存泄漏和內存溢出。C# GC是由公共語言運行時(CLR)提供的一個組件,它可以控制托管堆上對象的分配和釋放¹²。
C# GC的主要特點有以下幾個
- C# GC是一種分代式垃圾回收器,它將對象按照其存活時間分為0代、1代和2代三個代數,每個代數有自己的內存區域。一般來說,0代對象的生命周期最短,2代對象的生命周期最長。C# GC在進行垃圾回收時,會優先回收0代對象,然后是1代對象,最后是2代對象。這樣可以提高垃圾回收的效率和性能。
- C# GC是一種並發式垃圾回收器,它可以在用戶線程運行的同時進行部分垃圾回收工作,從而減少用戶線程被暫停的時間。C# GC有兩種並發模式:工作站模式和服務器模式。工作站模式適用於單處理器或客戶端應用程序,服務器模式適用於多處理器或服務器應用程序。
- C# GC是一種可配置的垃圾回收器,它可以根據應用程序的需求和環境來調整垃圾回收的策略和參數。例如,可以通過設置GCSettings.LatencyMode屬性來控制垃圾回收對應用程序響應時間的影響,或者通過設置GCSettings.LargeObjectHeapCompactionMode屬性來控制大對象堆的壓縮方式。
1.關於垃圾回收
.NET 的垃圾回收器管理應用程序的內存分配和釋放。 每當有對象新建時,公共語言運行時都會從托管堆為對象分配內存。 只要托管堆中有地址空間,運行時就會繼續為新對象分配空間。 不過,內存並不是無限的。 垃圾回收器最終必須執行垃圾回收來釋放一些內存。 垃圾回收器的優化引擎會根據所執行的分配來確定執行回收的最佳時機。 執行回收時,垃圾回收器會在托管堆中檢查應用程序不再使用的對象,然后執行必要的操作來回收其內存。
垃圾回收:在公共語言運行時 (CLR) 中,垃圾回收器 (GC) 用作自動內存管理器。 垃圾回收器管理應用程序的內存分配和釋放。 對於使用托管代碼的開發人員而言,這就意味着不必編寫執行內存管理任務的代碼。 自動內存管理可解決常見問題,例如,忘記釋放對象並導致內存泄漏,或嘗試訪問已釋放對象的內存。
1.1 優點
垃圾回收器具有以下優點:
- 開發人員不必手動釋放內存。
- 有效分配托管堆上的對象。
- 回收不再使用的對象,清除它們的內存,並保留內存以用於將來分配。 托管對象會自動獲取干凈的內容來開始,因此,它們的構造函數不必對每個數據字段進行初始化。
- 通過確保對象不能使用另一個對象的內容來提供內存安全。
1.2 內存的基礎知識
C#.Net/CoreCLR性能調優,基准測試:https://www.ixigua.com/6951281664077234719
下面的列表總結了重要的 CLR 內存概念。
-
每個進程都有其自己單獨的虛擬地址空間。 同一台計算機上的所有進程共享相同的物理內存和頁文件(如果有)。
-
默認情況下,32 位計算機上的每個進程都具有 2 GB 的用戶模式虛擬地址空間。
-
作為一名應用程序開發人員,你只能使用虛擬地址空間,請勿直接操控物理內存。 垃圾回收器為你分配和釋放托管堆上的虛擬內存。
如果你編寫的是本機代碼,請使用 Windows 函數處理虛擬地址空間。 這些函數為你分配和釋放本機堆上的虛擬內存。
-
虛擬內存有三種狀態:
狀態 描述 Free 該內存塊沒有引用關系,可用於分配。 保留 內存塊可供你使用,並且不能用於任何其他分配請求。 但是,在該內存塊提交之前,你無法將數據存儲到其中。 已提交 內存塊已指派給物理存儲。 -
可能會存在虛擬地址空間碎片。 就是說地址空間中存在一些被稱為孔的可用塊。 當請求虛擬內存分配時,虛擬內存管理器必須找到滿足該分配請求的足夠大的單個可用塊。 即使有 2 GB 可用空間,2 GB 分配請求也會失敗,除非所有這些可用空間都位於一個地址塊中。
-
如果沒有足夠的可供保留的虛擬地址空間或可供提交的物理空間,則可能會用盡內存。
即使在物理內存壓力(即物理內存的需求)較低的情況下也會使用頁文件。 首次出現物理內存壓力較高的情況時,操作系統必須在物理內存中騰出空間來存儲數據,並將物理內存中的部分數據備份到頁文件中。 該數據只會在需要時進行分頁,所以在物理內存壓力較低的情況下也可能會進行分頁。
內存分配: 初始化新進程時,運行時會為進程保留一個連續的地址空間區域。 這個保留的地址空間被稱為托管堆。 托管堆維護着一個指針,用它指向將在堆中分配的下一個對象的地址。 最初,該指針設置為指向托管堆的基址。 托管堆上部署了所有引用類型。 應用程序創建第一個引用類型時,將為托管堆的基址中的類型分配內存。 應用程序創建下一個對象時,垃圾回收器在緊接第一個對象后面的地址空間內為它分配內存。 只要地址空間可用,垃圾回收器就會繼續以這種方式為新對象分配空間。
從托管堆中分配內存要比非托管內存分配速度快。 由於運行時通過為指針添加值來為對象分配內存,所以這幾乎和從堆棧中分配內存一樣快。 另外,由於連續分配的新對象在托管堆中是連續存儲,所以應用程序可以快速訪問這些對象。
內存釋放:
垃圾回收器的優化引擎根據所執行的分配決定執行回收的最佳時間。 垃圾回收器在執行回收時,會釋放應用程序不再使用的對象的內存。 它通過檢查應用程序的根來確定不再使用的對象。 應用程序的根包含線程堆棧上的靜態字段、局部變量、CPU 寄存器、GC 句柄和終結隊列。 每個根或者引用托管堆中的對象,或者設置為空。 垃圾回收器可以為這些根請求其余運行時。 垃圾回收器使用此列表創建一個圖表,其中包含所有可從這些根中訪問的對象。
不在該圖表中的對象將無法從應用程序的根中訪問。 垃圾回收器會考慮無法訪問的對象垃圾,並釋放為它們分配的內存。 在回收中,垃圾回收器檢查托管堆,查找無法訪問對象所占據的地址空間塊。 發現無法訪問的對象時,它就使用內存復制功能來壓縮內存中可以訪問的對象,釋放分配給不可訪問對象的地址空間塊。 在壓縮了可訪問對象的內存后,垃圾回收器就會做出必要的指針更正,以便應用程序的根指向新地址中的對象。 它還將托管堆指針定位至最后一個可訪問對象之后。
只有在回收發現大量的無法訪問的對象時,才會壓縮內存。 如果托管堆中的所有對象均未被回收,則不需要壓縮內存。
為了改進性能,運行時為單獨堆中的大型對象分配內存。 垃圾回收器會自動釋放大型對象的內存。 但是,為了避免移動內存中的大型對象,通常不會壓縮此內存。
主要分為二個步驟:
標記對象和壓縮托管堆
參考資料:https://www.cnblogs.com/wilber2013/p/4357910.html
1.3 垃圾回收的條件
當滿足以下條件之一時將發生垃圾回收:
- 系統具有低的物理內存。 這是通過 OS 的內存不足通知或主機指示的內存不足檢測出來。
- 由托管堆上已分配的對象使用的內存超出了可接受的閾值。 隨着進程的運行,此閾值會不斷地進行調整。
- 調用 GC.Collect 方法。 幾乎在所有情況下,你都不必調用此方法,因為垃圾回收器會持續運行。 此方法主要用於特殊情況和測試。
1.4 托管堆
在垃圾回收器由 CLR 初始化之后,它會分配一段內存用於存儲和管理對象。 此內存稱為托管堆(與操作系統中的本機堆相對)。
每個托管進程都有一個托管堆。 進程中的所有線程都在同一堆上為對象分配內存。
若要保留內存,垃圾回收器會調用 Windows VirtualAlloc 函數,並且每次為托管應用保留一個內存段。 垃圾回收器還會根據需要保留內存段,並調用 Windows VirtualFree 函數,將內存段釋放回操作系統(在清除所有對象的內存段后)。
堆上分配的對象越少,垃圾回收器必須執行的工作就越少。 分配對象時,請勿使用超出你需求的舍入值,例如在僅需要 15 個字節的情況下分配了 32 個字節的數組。
當觸發垃圾回收時,垃圾回收器將回收由非活動對象占用的內存。 回收進程會對活動對象進行壓縮,以便將它們一起移動,並移除死空間,從而使堆更小一些。 這將確保一起分配的對象全都位於托管堆上,從而保留它們的局部性。
垃圾回收的侵入性(頻率和持續時間)是由分配的數量和托管堆上保留的內存數量決定的。
此堆可視為兩個堆的累計:大對象堆和小對象堆。 大對象堆包含大小不少於 85,000 個字節的對象,這些對象通常是數組。 非常大的實例對象是很少見的。
1.5 代數
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)上運行,暫時段的大小發生相應變化。 下表顯示了暫時段的默認大小。
工作站/服務器 GC | 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 代對象可使用多個段(在內存允許的情況下進程所需的任意數量)。
從暫時垃圾回收中釋放的內存量限制為暫時段的大小。 釋放的內存量與死對象占用的空間成比例。
1.6 垃圾回收過程中發生的情況
垃圾回收分為以下幾個階段:
-
標記階段,找到並創建所有活動對象的列表。
-
重定位階段,用於更新對將要壓縮的對象的引用。
-
壓縮階段,用於回收由死對象占用的空間,並壓縮幸存的對象。 壓縮階段將垃圾回收中幸存下來的對象移至段中時間較早的一端。
因為第 2 代回收可以占用多個段,所以可以將已提升到第 2 代中的對象移動到時間較早的段中。 可以將第 1 代幸存者和第 2 代幸存者都移動到不同的段,因為它們已被提升到第 2 代。
通常,由於復制大型對象會造成性能代償,因此不會壓縮大型對象堆 (LOH)。 但是,在 .NET Core 和 .NET Framework 4.5.1 及更高版本中,可以根據需要使用 GCSettings.LargeObjectHeapCompactionMode 屬性按需壓縮大型對象堆。 此外,當通過指定以下任一項設置硬限制時,將自動壓縮 LOH:
- 針對容器的內存限制。
- GCHeapHardLimit 或 GCHeapHardLimitPercent 運行時配置選項。
垃圾回收器使用以下信息來確定對象是否為活動對象:
- 堆棧根。 由實時 (JIT) 編譯器和堆棧查看器提供的堆棧變量。 JIT 優化可以延長或縮短報告給垃圾回收器的堆棧變量內的代碼的區域。
- 垃圾回收句柄。 指向托管對象且可由用戶代碼或公共語言運行時分配的句柄。
- 靜態數據。 應用程序域中可能引用其他對象的靜態對象。 每個應用程序域都會跟蹤其靜態對象。
在垃圾回收啟動之前,除了觸發垃圾回收的線程以外的所有托管線程均會掛起。
下圖演示了觸發垃圾回收並導致其他線程掛起的線程。
1.7 非托管資源
對於應用程序創建的大多數對象,可以依賴垃圾回收自動執行必要的內存管理任務。 但是,非托管資源需要顯式清除。 最常用的非托管資源類型是包裝操作系統資源的對象,例如,文件句柄、窗口句柄或網絡連接。 雖然垃圾回收器可以跟蹤封裝非托管資源的托管對象的生存期,但卻無法具體了解如何清理資源。
創建封裝非托管資源的對象時,建議在公共 Dispose
方法中提供必要的代碼以清理非托管資源。 通過提供 Dispose
方法,對象的用戶可以在使用完對象后顯式釋放其內存。 使用封裝非托管資源的對象時,務必要在需要時調用 Dispose
。
還必須提供一種釋放非托管資源的方法,以防類型使用者忘記調用 Dispose
。 可以使用安全句柄來包裝非托管資源,也可以重寫 Object.Finalize() 方法。
2. 工作站和服務器垃圾回收
垃圾回收器可自行優化並且適用於多種方案。 不過,你可以基於工作負載的特征設置垃圾回收的類型。 CLR 提供了以下類型的垃圾回收:
-
工作站垃圾回收 (GC) 是為客戶端應用設計的。 它是獨立應用的默認 GC 風格。 對於托管應用(例如由 ASP.NET 托管的應用),由主機確定默認 GC 風格。
工作站垃圾回收既可以是並發的,也可以是非並發的。 並發(或后台 )垃圾回收使托管線程能夠在垃圾回收期間繼續操作。 后台垃圾回收替換 .NET Framework 4 及更高版本中的並行垃圾回收。
-
服務器垃圾回收,用於需要高吞吐量和可伸縮性的服務器應用程序。
- 在 .NET Core 中,服務器垃圾回收既可以是非並發也可以是后台執行。
- 在 .NET Framework 4.5 和更高版本中,服務器垃圾回收既可以是非並發也可以是后台執行。 在 .NET Framework 4 和以前的版本中,服務器垃圾回收非並行運行。
下圖演示了服務器上執行垃圾回收的專用線程:
性能注意事項
工作站 GC
以下是工作站垃圾回收的線程處理和性能注意事項:
-
回收發生在觸發垃圾回收的用戶線程上,並保留相同優先級。 因為用戶線程通常以普通優先級運行,所以垃圾回收器(在普通優先級線程上運行)必須與其他線程競爭 CPU 時間。 (運行本機代碼的線程不會由於服務器或工作站垃圾回收而掛起。)
-
工作站垃圾回收始終用於只有一個處理器的計算機,無論配置設置如何。
服務器 GC
以下是服務器垃圾回收的線程處理和性能注意事項:
- 回收發生在以
THREAD_PRIORITY_HIGHEST
優先級運行的多個專用線程上。 - 為每個 CPU 提供一個用於執行垃圾回收的一個堆和專用線程,並將同時回收這些堆。 每個堆都包含一個小對象堆和一個大對象堆,並且所有的堆都可由用戶代碼訪問。 不同堆上的對象可以相互引用。
- 因為多個垃圾回收線程一起工作,所以對於相同大小的堆,服務器垃圾回收比工作站垃圾回收更快一些。
- 服務器垃圾回收通常具有更大的段。 但是,這是通常情況:段大小特定於實現且可能更改。 調整應用程序時,不要假設垃圾回收器分配的段大小。
- 服務器垃圾回收會占用大量資源。 例如,假設在一台有 4 個處理器的計算機上,運行着 12 個使用服務器 GC 的進程。 如果所有進程碰巧同時回收垃圾,它們會相互干擾,因為將在同一個處理器上調度 12 個線程。 如果進程處於活動狀態,則最好不要讓它們都使用服務器 GC。
3. 后台垃圾回收
在后台垃圾回收 (GC) 中,在進行第 2 代回收的過程中,將會根據需要收集暫時代(第 0 代和第 1 代)。 后台垃圾回收是在一個或多個專用線程上執行的,具體取決於它是后台還是服務器 GC,它只適用於第 2 代回收。
默認啟用后台垃圾回收。 可以在 .NET Framework 應用中使用 gcConcurrent 配置設置或 .NET Core 和 .NET 5 及更高版本應用中的 System.GC.Concurrent 來啟用或禁用后台垃圾回收。
備注
后台垃圾回收替換在 .NET Framework 4 及更高版本中可用的[並行垃圾回收]。 在 .NET Framework 4 中,僅支持工作站垃圾回收。 從 .NET Framework 4.5 開始,后台垃圾回收可用於工作站和服務器垃圾回收 。
后台垃圾回收期間對暫時代的回收稱為“前台”垃圾回收。 發生前台垃圾回收時,所有托管線程都將被掛起。
當后台垃圾回收正在進行並且你已在第 0 代中分配了足夠的對象時,CLR 將執行第 0 代或第 1 代前台垃圾回收。 專用的后台垃圾回收線程將在常見的安全點上進行檢查以確定是否存在對前台垃圾回收的請求。 如果存在,則后台回收將掛起自身以便前台垃圾回收可以發生。 在前台垃圾回收完成之后,專用的后台垃圾回收線程和用戶線程將繼續。
后台垃圾回收可以消除並發垃圾回收所帶來的分配限制,因為在后台垃圾回收期間,可發生暫時垃圾回收。 后台垃圾回收可以刪除暫存世代中的死對象。 如果需要,它還可以在第 1 代垃圾回收期間擴展堆。
后台工作站與服務器 GC
從 .NET Framework 4.5 開始,后台垃圾回收可用於服務器 GC。 服務器 GC 是服務器垃圾回收的默認模式。
后台服務器垃圾回收與后台工作站垃圾回收具有類似功能,但有一些不同之處:
- 后台工作區域垃圾回收使用一個專用的后台垃圾回收線程,而后台服務器垃圾回收使用多個線程。 通常一個邏輯處理器有一個專用線程。
- 不同於工作站后台垃圾回收線程,這些后台服務器 GC 線程不會超時。
下圖顯示對獨立專用線程執行的后台工作站垃圾回收:
下圖顯示對獨立專用線程執行的后台服務器垃圾回收:
並行垃圾回收
在.net更高的版本中,后台垃圾回收取代了並行垃圾回收。
4.大型對象堆
4.1Windows 系統上的大型對象堆
.NET 垃圾回收器 (GC) 將對象分為小型和大型對象。 如果是大型對象,它的某些特性將比對象較小時顯得更為重要。 例如,壓縮大型對象—(也就是在內存中將其復制到堆上的其他位置)—的費用相當高。 因此,垃圾回收器將大型對象放置在大型對象堆 (LOH) 上。 本文將討論符合什么條件的對象才能稱之為大型對象,如何回收大型對象,以及大型對象具備哪些性能意義。
LOH:大型對象堆
SOH:小型對象堆
對象如何在LOH上結束
如果對象的大小大於或等於 85,000 字節,將被視為大型對象。
垃圾回收器是分代回收器。 它包含三代:第 0 代、第 1 代和第 2 代。 包含 3 代的原因是,在優化良好的應用中,大部分對象都在第 0 代就清除了。 例如,在服務器應用中,與每個請求相關的分配應在請求完成后清除。 仍存在的分配請求將轉到第 1 代,並在那里進行清除。 從本質上講,第 1 代是新對象區域與生存期較長的對象區域之間的緩沖區。
小型對象始終在第 0 代中進行分配,或者根據它們的生存期,可能會提升為第 1 代或第 2 代。 大型對象始終在第 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 代和第 2 代回收未處理的對象)中“分配”對象。
觸發垃圾回收后,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 代和第 1 代 GC。
圖 2 顯示了第 2 代 GC 發現 Obj1
和 Obj2
被清除后,GC 在內存中形成了相鄰的可用空間,由 Obj1
和 Obj2
占用,然后用於滿足 Obj4
的分配要求。 從最后一個對象 Obj3
到此段末尾的空間仍可用於滿足分配請求。
圖 2:第 2 代 GC 后
如果沒有足夠的可用空間來容納大型對象分配請求,GC 首先嘗試從操作系統獲取更多段。 如果失敗了,它將觸發第 2 代 GC,試圖釋放部分空間。
在第 1 代或第 2 代 GC 期間,垃圾回收器會通過調用 VirtualFree 功能將不包含活動對象的段釋放回操作系統。 將退回最后一個活動對象到段末尾的空間(第 0 代/第 1 代存在的短暫段上的空間除外,垃圾回收器會在該段上會保存部分提交內容,因為應用程序將在其中立即分配)。 而且,盡管已重置可用空間,但仍會提交它們,這意味着操作系統無需將其中的數據重新寫入磁盤。
由於 LOH 僅在第 2 代 GC 期間進行回收,所以 LOH 段僅在此類 GC 期間可用。 圖 3 說明了一種情況,在此情況下,垃圾回收器將某段(段 2)釋放回操作系統並且退回剩余段上更多的空間。 如果需要使用該段末尾的已退回空間來滿足大型對象分配請求,它會再次提交該內存。 (有關提交/退回的解釋說明,請參閱 VirtualAlloc 的文檔)。
圖 3:第 2 代 GC 后的 LOH
4.2何時收集大型對象
通常情況下,出現以下三種情形中的任一情況,都會執行 GC:
-
分配超出第 0 代或大型對象閾值。
閾值是某代的屬性。 垃圾回收器在其中分配對象時,會為代設置閾值。 超出閾值后,會在該代上觸發 GC。 因此,分配小型或大型對象時,需要分別使用第 0 代和 LOH 的閾值。 當垃圾回收器分配到第 1 代和第 2 代中時,將使用它們的閾值。 運行此程序時,會動態調整這些閾值。
這是典型情況,大部分 GC 執行都因為托管堆上的分配。
-
調用 GC.Collect 方法。
如果調用無參數 GC.Collect() 方法,或另一個重載作為參數傳遞到 GC.MaxGeneration,將會一起收集 LOH 和剩余的托管堆。
-
系統處於內存不足的狀況。
垃圾回收器收到來自操作系統 的高內存通知時,會發生以上情況。 如果垃圾回收器認為執行第 2 代 GC 會有效率,它將觸發第 2 代。
4.3LOH性能意義
大型對象堆上的分配通過以下幾種方式影響性能。
-
分配成本。
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
。 而垃圾回收器無需查看左側節點和右側節點的任何引用。
在這三種因素中,前兩個通常比第三個更重要。 因此,建議分配重復使用的大型對象池,而不是分配臨時大型對象。
4.4 收集 LOH 的性能數據
收集特定區域的性能數據之前,應完成以下操作:
- 找到應查看此區域的證據。
- 排查你知道的其他區域,確保未發現可解釋上述性能問題的內容。
參閱博客嘗試找出解決方案之前先了解問題獲取內存和 CPU 的基礎知識的詳細信息。
可使用以下工具來收集 LOH 性能數據:
.NET CLR 內存性能計數器
這些性能計數器通常是調查性能問題的第一步(但是推薦使用 ETW 事件)。 通過添加所需計數器配置性能監視器,如圖 4 所示。 與 LOH 相關的是:
-
第 2 代回收次數
顯示自進程開始起第 2 代 GC 發生的次數。 此計數器在第 2 代回收結束時遞增(也稱為完整垃圾回收)。 此計數器顯示上次觀測的值。
-
大型對象堆大小
以字節顯示當前大小,包括 LOH 的可用空間。 此計數器在垃圾回收結束時更新,不在每次分配時更新。
查看性能計數器的常用方法是使用性能監視器 (perfmon.exe)。 使用“添加計數器”可為關注的進程添加感興趣的計數器。 可將性能計數器數據保存在日志文件中,如圖 4 所示:
圖 4:第 2 代 GC 后的 LOH
也可以編程方式查詢性能計數器。 大部分人在例行測試過程中都采用此方式進行收集。 如果發現計數器顯示的值不正常,則可以使用其他方法獲得更多詳細信息以幫助調查。
ETW 事件
垃圾回收器提供豐富的 ETW 事件集,幫助了解堆的工作內容和工作原理。 以下博客文章演示了如何使用 ETW 收集和了解 GC 事件:
若要標識由臨時 LOH 分配造成的過多第 2 代 GC 次數,請查看 GC 的“觸發原因”列。 有關僅分配臨時大型對象的簡單測試,可使用以下 PerfView 命令行收集 ETW 事件的信息:
控制台復制
perfview /GCCollectOnly /AcceptEULA /nogui collect
結果類似於以下類容:
圖 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 上設置一個斷點,查看是誰調用了它們。 例如,如果想知道誰曾嘗試從操作系統分配大於 8MBB 的虛擬內存塊,可按以下方式設置斷點:
控制台復制
bp kernel32!virtualalloc "j (dwo(@esp+8)>800000) 'kb';'g'"
只有在分配大小大於 8MB (0x800000) 的情況下調用 VirtualAlloc 時,此命令才會進入調試器並顯示調用堆棧。
CLR 2.0 增加了稱為“VM 囤積”的功能,用於頻繁獲取和釋放段(包括在大型和小型對象堆上)的情況。 若要指定 VM 囤積,可通過托管 API 指定稱為 STARTUP_HOARD_GC_VM
的啟動標記。 CLR 退回這些段上的內存並將其添加到備用列表中,而不會將該空段釋放回操作系統。 (請注意 CLR 不會針對太大型的段執行此操作。)CLR 稍后將使用這些段來滿足新段請求。 下一次應用需要新段時,CLR 將使用此備用列表中的某個足夠大的段。
VM 囤積還可用於想要保存已獲取段的應用程序(例如屬於系統上運行的主要應用的部分服務器應用),以避免內存不足的異常。
強烈建議你在使用此功能時認真測試應用程序,以確保應用程序的內存使用情況比較穩定。
垃圾回收通知
在有些情況下,公共語言運行時執行的完整垃圾回收(即第 2 代回收)可能會對性能產生負面影響。 特別是,處理大量請求的服務器可能會出現此問題;在這種情況下,長時間垃圾回收會導致請求超時。為了防止在關鍵時期發生完全回收,可以接收即將執行完全垃圾回收的通知,再采取措施將工作負載重定向到另一個服務器實例。 也可以自行誘導回收,前提是當前服務器實例不需要處理請求。
弱引用
強引用:如果應用程序的代碼可以訪問一個正由該程序使用的對象,垃圾回收器就不能回收該對象, 那么,就認為應用程序對該對象具有強引用。
弱引用允許應用程序訪問對象,同時也允許垃圾回收器收集相應的對象。 如果不存在強引用,則弱引用的有限期只限於收集對象前的一個不確定的時間段
常問的問題:
什么樣的對象需要垃圾回收?
托管資源+引用類型
什么是托管資源和非托管資源?
托管資源:托管的就是CLR控制的。包括new的對象,string字符串,變量
非托管資源:非托管不是CLR能控制的,數據庫連接,文件流,句柄,打印機連接
using(SqlConnection)被C#封裝了管理了哪個非托管的數據庫連接資源。
哪些對象能被GC回收?
對象訪問不到了,那就可以被回收了
程序--入口--去找對象--建立對象圖--訪問不到的就是垃圾
對象時如何分配在堆上?
連續分配在堆上面,每次分配就先檢查空間夠不夠
什么時候執行GC?
new對象時--臨界點
GC.Collect強制GC
程序退出時會GC
參考資料
https://www.cnblogs.com/brucebi/archive/2013/04/16/3024136.html
http://www.cnblogs.com/brucebi/archive/2013/04/16/3024136.html