了解.NET中的垃圾回收
原文來自互聯網,由長沙DotNET技術社區編譯。盡管這是一篇來自2009年的古老的文章,但或許能夠對你理解GC產生一些作用。?
了解.NET中的垃圾回收
一旦了解了.NET的垃圾收集器是如何工作的,那么可能會觸及.NET應用程序的一些更為神秘的問題時,進行原因分析就會變得更加清楚。NET已經不在提供顯式內存管理的方式,但在開發.NET應用程序時,仍然有必要分析內存的使用情況,以便避免與內存相關的錯誤和某些性能問題。
.NET的垃圾收集器已在Windows應用程序中作為顯式內存管理和內存泄漏的結束而開放給我們:這個想法是,在后臺運行垃圾收集器的情況下,開發人員不再需要擔心管理它們創建的對象的生命周期–應用程序完成處理后,垃圾收集器將對其進行處理。
但是,實際情況要復雜得多。垃圾收集器無疑解決了非托管程序中最常見的泄漏-由開發人員在完成使用后忘記釋放內存而引起的泄漏。它還解決了內存釋放過早的相關問題,但是當垃圾收集器對開發人員對對象是否仍然處于“活動狀態”并且能夠進行開發時有不同的看法時,解決該問題的方式可能導致內存泄漏。要使用的。解決這些問題之前,您需要對收集器的工作方式有所了解。
垃圾收集器如何工作
那么,垃圾收集器如何實現其魔力?基本思想非常簡單:它檢查對象在內存中的布局方式,并通過遵循一系列引用來標識正在運行的程序可以“訪問”的所有那些對象。
當垃圾回收開始時,它將查看一組稱為“ GC根”的引用。這些是由于某種原因總是可以訪問的內存位置,并且包含對程序創建的對象的引用。它將這些對象標記為“活動”,然后查看它們引用的所有對象。它也將這些標記為“實時”。它以這種方式繼續,遍歷它知道是“活動”的所有對象。它將它們引用的所有內容都標記為也被使用,直到找不到其他對象為止。
如果某個對象或其超類之一的字段包含另一個對象,則該對象由垃圾收集器標識為引用另一個對象。
一旦知道了所有這些活動對象,就可以丟棄所有剩余的對象,并將空間重新用于新對象。.NET壓縮內存,以確保沒有間隙(有效地壓縮丟棄的對象不存在)–這意味著空閑內存始終位于堆的末尾,并可以非常快速地分配新對象。
GC根本身不是對象,而是對對象的引用。GC根引用的任何對象將自動在下一個垃圾回收中保留下來。.NET中有四種主要的根:
當前正在運行的方法中的局部變量被視為GC根。這些變量引用的對象始終可以通過聲明它們的方法立即訪問,因此必須保留它們。這些根的生命周期可以取決于程序的構建方式。在調試版本中,局部變量的持續時間與方法在堆棧上的時間一樣長。在發行版本中,JIT能夠查看程序結構以找出執行過程中該方法可以使用變量的最后一點,并在不再需要該變量時將其丟棄。這種策略并不總是使用,可以通過例如在調試器中運行程序來關閉。
靜態變量也始終被視為GC根。聲明它們的類可以隨時訪問它們引用的對象(如果是公共的,則可以訪問程序的其余部分),因此.NET將始終保持它們不變。聲明為“線程靜態”的變量僅會在該線程運行時持續存在。
如果通過互操作將托管對象傳遞給非托管COM +庫,則該對象也將成為具有引用計數的GC根。這是因為COM +不進行垃圾收集:它使用引用計數系統;通過將引用計數設置為0,一旦COM +庫完成了該對象,它將不再是GC根目錄,并且可以再次收集。
如果對象具有終結器,則在垃圾回收器確定該對象不再“處于活動狀態”時,不會立即將其刪除。相反,它成為一種特殊的根,直到.NET調用了finalizer方法。這意味著這些對象通常需要從內存中刪除一個以上的垃圾回收,因為它們在第一次發現未使用時仍將生存。
對象圖
總體而言,.NET中的內存形成了一個復雜的,打結的引用和交叉引用圖。這可能使得很難確定特定對象使用的內存量。例如,List對象使用的內存非常小,因為List?類只有幾個字段。但是,其中之一是列表中的對象數組:如果列表中有許多條目,則這可能會很大。這幾乎總是由列表“獨占”,因此關系非常簡單:列表的總大小是小的初始對象和它引用的大數組的大小。但是,數組中的對象可能完全是另一回事:很可能存在通過內存的其他路徑來訪問它們。在這種情況下,
當循環引用開始起作用時,事情變得更加混亂。
?
在開發代碼時,通常將內存視為組織為更容易理解的結構:從各個根開始的樹:
確實,以這種方式進行思考確實使(更確實可能)思考對象在內存中的布局方式。這也是編寫程序或使用調試器時表示數據的方式,但這很容易忘記一個對象可以附加到多個根。這通常是.NET中內存泄漏的來源:開發人員忘記或從未意識到,一個對象錨定到多個根。考慮一下此處所示的情況:將GC root 2設置為null實際上不會允許垃圾收集器刪除任何對象,這可以從查看完整圖形中看到,而不能從樹中看到。
內存剖析器可以從另一個角度查看圖形,就像樹根植于單個對象并向后跟隨引用以將GC根放在葉子上一樣。對于根2引用的ClassC對象,我們可以向后跟隨引用以獲取下圖:
?
通過這種方式的思考表明,ClassC對象具有兩個最終的“所有者”,在垃圾收集器將其刪除之前,這兩個對象都必須放棄它。一旦將GC根目錄2設置為null,就可以斷開GC根目錄3與該對象之間的任何鏈接,以便將其刪除。
在實際的.NET應用程序中,這種情況很容易出現。最常見的是,數據對象被用戶界面中的元素引用,但在數據處理完畢后不會被刪除。這種情況并不是很泄漏:當用新數據更新UI控件時,將回收內存,但是這可能意味著應用程序使用的內存比預期的要多得多。事件處理程序是另一個常見原因:很容易忘記一個對象的壽命至少與它從中接收事件的對象一樣長,對于某些全局事件處理程序(如Application類中的事件),這種情況永遠存在。
實際的應用程序,尤其是那些具有用戶界面組件的應用程序,具有比這復雜得多的圖形。甚至可以從大量不同的地方引用對話框中的標簽之類的簡單內容…
?很容易看到偶然的物體如何在迷宮中丟失。
垃圾收集器的局限性
仍在引用的未使用對象
.NET中垃圾收集器的最大局限性是一個細微的限制:雖然它可以檢測和刪除未使用的對象,但實際上它會找到未引用的對象。這是一個重要的區別:程序可能永遠不會再引用對象。但是,盡管有一些路徑導致它可能仍被使用,但它永遠不會從內存中釋放出來。這導致內存泄漏;在.NET中,當將不再使用的對象保持引用狀態時,會發生這些情況。
盡管內存使用率上升的癥狀很明顯,但這些泄漏的來源可能很難發現。有必要確定哪些未使用的對象保留在內存中,然后跟蹤引用以找出為什么不收集它們。內存分析器對于此任務至關重要:通過比較發生泄漏時的內存狀態,可以找到麻煩的未使用對象,但是沒有調試器可以向后跟蹤對象引用。
垃圾收集器旨在處理大量資源,也就是說,釋放對象的位置無關緊要。在現代系統上,內存屬于這一類(何時回收內存無關緊要,只要及時完成以防止新分配失敗)。仍然有一些資源不屬于此類:例如,需要快速關閉文件句柄以避免引起應用程序之間的共享沖突。這些資源不能由垃圾收集器完全管理,因此.NET為管理這些資源的對象提供Dispose()方法以及using()構造。在這些情況下,對象的稀缺資源可通過實施Dispose?方法,但是緊要的內存要少得多,然后由垃圾回收器釋放。
Dispose意味著.NET沒有什么特別的,因此仍必須取消引用已處置的對象。這使已處置但尚未回收的對象成為內存泄漏源的良好候選對象。
堆的碎片
.NET中一個鮮為人知的限制是大對象堆的限制。成為該堆一部分的對象不會在運行時移動,這可能導致程序過早地耗盡內存。當某些對象的壽命比其他對象長時,這將導致堆在對象過去所在的位置形成孔-這稱為碎片。當程序要求一個大的內存塊,但堆變得非常分散,以至于沒有單個內存區域足以容納它時,就會發生問題。內存分析器可以估計程序可以分配的最大對象:如果該對象正在下降,則很可能是原因。一個OutOfMemoryException當程序顯然具有大量可用內存時,通常會發生由碎片引起的錯誤–在32位系統上,進程應至少能夠使用1.5Gb,但是由于碎片導致的故障通常會在使用該碎片之前開始發生很多內存。
碎片化的另一個征兆是.NET通常必須保留分配給應用程序的空洞所使用的內存。這顯然導致它使用比在任務管理器中查看所需的內存更多的內存。這種效果通常相對來說是無害的:Windows非常擅長于意識到未被占用的孔所占用的內存并將其分頁,并且如果碎片沒有惡化,則程序將不會耗盡內存。但是,對于用戶而言,這看起來并不好,他們可能會認為該應用程序浪費且“ blo腫”。當探查器顯示程序分配的對象僅使用少量內存,而任務管理器顯示該進程占用大量空間時,通常會發生這種情況。
垃圾收集器的性能
在性能方面,垃圾收集系統的最重要特征是垃圾收集器可以隨時開始執行。這使它們不適用于定時至關重要的情況,因為任何操作的定時都可能被收集器的操作所拋棄。
.NET收集器有兩種主要的操作模式:并發和同步(有時稱為工作站和服務器)。默認情況下,并發垃圾收集用于桌面應用程序,同步用于服務器應用程序(例如ASP.NET)。
在并發模式下,.NET將嘗試避免在進行收集時停止正在運行的程序。這意味著在給定的時間內應用程序可以完成的總次數較少,但應用程序不會暫停。這對交互式應用程序很有用,在交互應用程序中,給用戶留下印象,即應用程序應立即做出響應,這一點很重要。
在同步模式下,.NET將在垃圾收集器運行時掛起正在運行的應用程序。實際上,這總體上比并發模式更有效–垃圾回收花費相同的時間,但是不必與程序繼續運行進行競爭–但是,這意味著必須執行完整的回收時會有明顯的暫停。。
如果默認設置不合適,則可以在應用程序的配置文件中設置垃圾收集器的類型。當更重要的是應用程序具有高吞吐量而不是顯示響應時,選擇同步收集器可能很有用。
在大型應用程序中,垃圾收集器需要處理的對象數量會變得非常大,這意味著訪問和重新排列所有對象都將花費很長時間。為了解決這個問題,.NET使用了“分代”垃圾收集器,該垃圾收集器試圖將優先級賦予較小的一組對象。這個想法是,最近創建的對象更有可能被快速釋放,因此,當試圖釋放內存時,分代垃圾收集器會優先處理它們,因此.NET首先查看自上一次垃圾收集以來已分配的對象,并且只會開始如果無法通過這種方式釋放足夠的空間,請考慮使用較舊的對象。
如果.NET可以自行選擇收集時間,則此系統效果最佳,并且如果GC.Collect調用()會中斷該系統,因為這通常會導致新對象過早地變舊,這增加了在不久的將來再次進行昂貴的完整收集的可能性。
具有終結器的類也會破壞垃圾收集器的平穩運行。這些類的對象不能立即刪除:相反,它們進入終結器隊列,并在運行終結器后從內存中刪除。這意味著它們所引用的任何對象(以及那些對象所引用的任何對象,依此類推)至少也必須在此之前保留在內存中,并且在內存再次可用之前需要兩次垃圾回收。如果該圖包含帶有終結器的許多對象,則這可能意味著垃圾收集器需要多次通過才能完全釋放所有未引用的對象。
有一個避免此問題的簡單方法:IDisposable在可終結類上實現,將完成對象所需的操作移到Dispose()方法中并GC.SuppressFinalize()在最后調用。然后可以修改終結器以調用該Dispose()方法。GC.SuppressFinalize()告訴垃圾回收器,該對象不再需要終結,可以立即被垃圾回收,這可以導致更快地回收內存。
結論
如果您花一些時間了解垃圾收集器的工作方式,則更容易理解應用程序中的內存和性能問題。它表明,盡管.NET減輕了內存管理的負擔,但并不能完全消除跟蹤和管理資源的需求。但是,使用內存分析器來診斷和修復.NET中的問題更加容易。考慮到.NET在開發中盡早管理內存的方式可以幫助減少問題,但是即使那樣,由于框架或第三方庫的復雜性,此類問題仍然可能出現。
總結
以上是生活随笔為你收集整理的了解.NET中的垃圾回收的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: dotNET Core 3.X 请求处理
- 下一篇: 数字化演化历史