wpf绑定treeview 带查找_如何查找,修复和避免C#.NET中内存泄漏的8个最佳实践
本文來源:https://michaelscodingspot.com/find-fix-and-avoid-memory-leaks-in-c-net-8-best-practices/
從事大型企業項目的任何人都知道內存泄漏就像是大型酒店中的老鼠。當它們很少時,您可能不會注意到,但是您必須始終保持警惕,以防它們過多,闖入廚房并制造混亂。
查找,修復和學習避免內存泄漏是一項重要技能。
我將列出我和高級.NET開發人員為我提供建議的8種最佳實踐技術,這些技術將教您檢測應用程序中何時存在內存泄漏問題,查找特定的內存泄漏并進行修復。最后,我將介紹監視和報告已部署程序的內存泄漏的策略。
定義.NET中的內存泄漏
在垃圾收集環境中,術語“內存泄漏”有點違反直覺。當有垃圾收集器(GC)負責收集所有內容時,我的內存為何還會泄漏?
有兩個相關的核心原因。第一個核心原因是當您具有仍被引用但實際上未使用的對象時。由于已引用它們,垃圾收集器將不會收集它們,并且它們將永久保留,占用內存。例如,當您注冊事件但從不注銷時,可能會發生這種情況。
第二個原因是當您以某種方式分配非托管內存(沒有垃圾回收)并且不釋放它時。這并不難做到。.NET本身有很多分配非托管內存的類。幾乎所有涉及流,圖形,文件系統或網絡調用的操作都是在后臺進行的。通常,這些類實現** Dispose **方法,該方法釋放內存(稍后再討論)。您可以使用特殊的.NET類(如Marshal)或PInvoke(有一個進一步的示例)輕松地自己分配非托管內存。
讓我們進入我的最佳實踐技術列表:
1.使用診斷工具窗口檢測內存泄漏問題
如果您去調試 | Windows | 顯示診斷工具,您將看到此窗口。如果您像我一樣,則可能在安裝Visual Studio之后看到了此工具窗口,立即關閉了它,再也沒有想到它。診斷工具窗口可能會非常有用。它可以輕松地幫助您檢測兩個問題:內存泄漏和GC壓力。
當您有內存泄漏時,“進程內存”圖如下所示:
圖片
從頂部的黃線可以看到GC正在嘗試釋放內存,但它仍在不斷上升。
當您具有GC Pressure時,過程內存圖如下所示:
圖片
“ GC壓力”是在創建新對象并將它們處置得太快而導致垃圾收集器無法跟上時。如圖所示,內存已接近極限,GC突發非常頻繁。
您將無法通過這種方式找到特定的內存泄漏,但是您可以檢測到內存泄漏問題,這本身就很有用。在Enterprise Visual Studio中,“診斷”窗口還包括一個內置的內存探查器,該探查器確實可以查找特定的泄漏。我們將在最佳實踐#3中討論內存分析。
2.使用任務管理器,Process Explorer或PerfMon檢測內存泄漏問題
檢測主要內存泄漏問題的第二種最簡單方法是使用任務管理器或Process Explorer(來自SysInternals)。這些工具可以顯示您的進程使用的內存量。如果它隨著時間不斷增加,則可能是內存泄漏。
圖片
性能監視器是有點難以利用[1],但能證明你的內存使用量隨時間的一個很好的曲線圖。這是我的應用程序的圖形,它不停地分配內存而不釋放它。我正在使用過程 | 專用字節計數器。
圖片
請注意,此方法眾所周知是不可靠的。您可能只是因為GC尚未收集內存而增加了內存使用量。還有共享內存和私有內存的問題,因此您可能會錯過內存泄漏和/或診斷不是您自己的內存泄漏(說明[2])。最后,您可能將內存泄漏誤認為是GC Pressure。在這種情況下,您不會發生內存泄漏,但是創建和處理對象的速度如此之快,以至于GC無法跟上進度。
盡管有缺點,但我還是提到了這種技術,因為它既易于使用,有時又是唯一的工具。這也是一個不錯的指標,長時間觀察時出了點問題。
3.使用內存分析器檢測內存泄漏
內存分析器就像處理內存泄漏的廚師刀。它是查找和修復它們的主要工具。盡管其他技術可能更易于使用或更便宜(探查器許可證價格昂貴),但最好精通至少一個內存探查器以有效解決內存泄漏問題。
.NET內存分析器中的大人物 是:dotMemory,SciTech內存分析器 和 ANTS Memory Profiler。如果您擁有Visual Studio Enterprise,則還有一個“免費”分析器。
所有內存分析器都以類似的方式工作。您可以附加到正在運行的進程,也可以打開轉儲文件。探查器將為您的進程的當前內存堆創建一個快照。您可以通過各種方式分析快照,例如,以下是當前快照中所有已分配對象的列表:
圖片
您可以看到每種類型分配了多少實例,它們占用了多少內存以及GC Root的引用路徑。
GC根是GC無法釋放的對象,因此GC根引用所引用的所有內容也無法釋放。當前活動線程的靜態對象和本地對象是GC根。在了解.NET中的垃圾收集中了解更多信息。
最快,最有用的性能分析技術是比較內存應返回相同狀態的2個快照。在操作之前拍攝第一個快照,在操作之后拍攝另一個快照。確切的步驟是:
1.從應用程序中的某種空閑狀態開始。這可能是主菜單或類似的東西。2.通過附加到進程或保存轉儲,使用Memory Profiler拍攝快照。3.運行懷疑會導致內存泄漏的操作。返回到空閑狀態。4.拍攝第二張快照。5.將這兩個快照與您的內存分析器進行比較。6.研究新創建的實例,它們很可能是內存泄漏。檢查“ GC根目錄的路徑”,并嘗試了解為什么未釋放這些對象。
這是一個很棒的視頻,其中在SciTech內存分析器 中比較了2個快照,并發現了內存泄漏:
4.使用“ Make Object ID”查找內存泄漏
在上一篇文章5避免C#.NET中的事件造成內存泄漏的技術中,您應該知道[3] 我展示了一種通過在類Finalizer中放置斷點來查找內存泄漏的技術。在這里,我將向您展示一種類似的方法,該方法更易于使用,并且不需要更改代碼。這利用了調試器的Make Object ID功能和Instant Window。
假設您懷疑某個類存在內存泄漏。換句話說,您懷疑在運行特定方案后,此類仍保持引用狀態,并且GC從未收集過此類。要確定GC是否真正收集了它,請按照下列步驟操作:
1.在創建類實例的地方放置一個斷點。2.將鼠標懸停在變量上以打開調試器的數據提示,然后右鍵單擊并使用Make Object ID。您可以在立即窗口$ 1中鍵入以查看是否正確創建了對象ID。3.完成本應從實例中釋放實例的方案。4.使用已知的魔術線強制進行GC收集
GC.Collect();???GC.WaitForPendingFinalizers(); GC.Collect();5. 原文中有兩端視頻,由于微信審核的緣故,無法上傳,有興趣的可以查看原文。
參考這個流程,您可以通過在立即窗口中鍵入魔術線來強制進行垃圾收集,從而使該技術成為完全的調試體驗,而無需更改代碼。
重要提示:這種做法在.NET Core 2.X調試器(問題[4])中不能很好地工作。強制在與對象分配相同的范圍內進行垃圾回收不會釋放該對象。通過將另一種方法中的垃圾回收強制超出范圍,您可以花費更多的精力。
5.當心常見的內存泄漏源
始終存在導致內存泄漏的風險,但是某些模式更有可能造成內存泄漏。我建議在使用這些工具時要格外小心,并使用最新的最佳做法等技術來主動檢查內存泄漏。
以下是一些較常見的違規者:
?.NET中的事件因導致內存泄漏而臭名昭著。您可以無辜地訂閱一個事件,甚至在不懷疑的情況下導致破壞性的內存泄漏。這個主題是如此重要,以至于我專門寫了整篇文章:您應該知道的5種避免C#.NET中的事件造成內存泄漏的技術[5]?特別是靜態變量,集合和靜態事件應該總是看起來可疑。請記住,所有靜態變量都是GC根,因此GC絕不會收集它們。?緩存功能 –任何類型的緩存機制都可以輕易導致內存泄漏。通過最終將高速緩存信息存儲在內存中,它將填滿并導致OutOfMemory異常。解決方案可以是定期刪除較早的緩存或限制緩存量。?WPF綁定可能很危險。經驗法則是始終綁定到DependencyObject或一種 INotifyPropertyChanged 賓語。如果您這樣做失敗,WPF將從靜態變量創建對綁定源(即ViewModel)的強引用,從而導致內存泄漏。此有用的StackOverflow線程中有關WPF綁定泄漏的更多信息?被捕獲的成員 –可能很明顯,事件處理程序方法意味著引用了一個對象,但是當變量在匿名方法中被捕獲時,也會被引用。這是內存泄漏的示例:
public class MyClass { private int _wiFiChangesCounter = 0; public MyClass(WiFiManager wiFiManager) { wiFiManager.WiFiSignalChanged += (s, e) => _wiFiChangesCounter++; } }?永不終止的線程 – 每個線程的活動堆棧都被視為GC根。這意味著在線程終止之前,GC不會收集其在堆棧上的變量的任何引用。這也包括計時器。如果您的Timer的滴答處理程序是一個方法,則該方法的對象被視為已引用,并且不會被收集。這是內存泄漏的示例:
public class MyClass { public MyClass(WiFiManager wiFiManager) { Timer timer = new Timer(HandleTick); timer.Change(TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(5)); } private void HandleTick(object state) { // do something } }有關此主題的更多信息,請查看我的文章8 .NET中導致內存泄漏的方法[6]。
6.使用“處置”模式來防止非托管內存泄漏
您的.NET應用程序不斷使用非托管資源。
.NET框架本身在很大程度上依賴于非托管代碼來進行內部操作,優化和Win32 API。
隨時使用Streams,Graphics或Files 對于 例如,您可能正在執行非托管代碼。使用非托管代碼的.NET框架類通常實現IDisposable。那是因為非托管資源需要明確地釋放,這發生在Dispose方法中。您唯一的工作就是記住并調用Dispose方法。如果可能,請使用using語句。
public void Foo(){ using (var stream = new FileStream(@"C:TempSomeFile.txt", FileMode.OpenOrCreate)) { // do stuff }// stream.Dispose() will be called even if an exception occurs}在使用語句轉換的代碼放到一個嘗試/最后的場景,在后面聲明的Dispose方法被調用的最后方法。但是,即使您不調用Dispose方法,這些資源也將被釋放,因為.NET類使用Dispose Pattern[7]。這基本上意味著,如果之前未調用Dispose,則在對象被垃圾回收時從Finalizer調用它。也就是說,如果您沒有內存泄漏并且確實調用了終結器。
當您自己分配非托管資源時,則絕對應該使用Dispose模式。這是一個例子:
public?class?MyClass?:?IDisposable{ private IntPtr _bufferPtr; public int BUFFER_SIZE = 1024 * 1024; // 1 MB private bool _disposed = false; public MyClass() { _bufferPtr = Marshal.AllocHGlobal(BUFFER_SIZE); } protected virtual void Dispose(bool disposing) { if (_disposed) return; if (disposing) { // Free any other managed objects here. } // Free any unmanaged objects here. Marshal.FreeHGlobal(_bufferPtr); _disposed = true; } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } ~MyClass() { Dispose(false); }}這種模式的重點是允許顯式處置資源。還要增加一種保護措施,如果未調用Dispose(),則將在垃圾回收期間(在Finalizer中)處置您的資源。
該GC.SuppressFinalize(這)也很重要。如果該對象已經存在,則可以確保終結器未在垃圾回收上被調用處置。使用終結器的對象將以不同的方式釋放,并且成本更高。將終結器添加到稱為F-Reachable-Queue的對象中,這使該對象在額外的GC生成后仍然存在。還有其他并發癥[8]。
7.從代碼添加內存遙測
有時,您可能想定期記錄您的內存使用情況。也許您懷疑生產服務器存在內存泄漏。當您的內存達到一定限制時,您可能想采取一些措施。或者,也許您只是養成監視內存的好習慣。
我們可以從應用程序本身中獲取很多信息。使用當前的內存很簡單:
Process currentProc = Process.GetCurrentProcess(); var bytesInUse = currentProc.PrivateMemorySize64;有關更多信息,可以使用用于PerfMon的PerformanceCounter類:
PerformanceCounter ctr1 = new PerformanceCounter("Process", "Private Bytes", Process.GetCurrentProcess().ProcessName); PerformanceCounter ctr2 = new PerformanceCounter(".NET CLR Memory", "# Gen 0 Collections", Process.GetCurrentProcess().ProcessName); PerformanceCounter ctr3 = new PerformanceCounter(".NET CLR Memory", "# Gen 1 Collections", Process.GetCurrentProcess().ProcessName); PerformanceCounter ctr4 = new PerformanceCounter(".NET CLR Memory", "# Gen 2 Collections", Process.GetCurrentProcess().ProcessName); PerformanceCounter ctr5 = new PerformanceCounter(".NET CLR Memory", "Gen 0 heap size", Process.GetCurrentProcess().ProcessName); //... Debug.WriteLine("ctr1 = " + ctr1 .NextValue()); Debug.WriteLine("ctr2 = " + ctr2 .NextValue()); Debug.WriteLine("ctr3 = " + ctr3 .NextValue()); Debug.WriteLine("ctr4 = " + ctr4 .NextValue()); Debug.WriteLine("ctr5 = " + ctr5 .NextValue());可從任何perfMon計數器獲得信息,這是很多信息。但是,您可以更深入。CLR MD(Microsoft.Diagnostics.Runtime)允許您檢查當前的內存堆并獲取任何可能的信息。例如,您可以打印內存中所有已分配的類型,包括實例計數,根目錄路徑等。您幾乎從代碼中獲得了一個內存探查器。
要了解使用CLR MD可以實現的目標,請查看Dudi Keleti的DumpMiner。
所有這些信息都可以記錄到文件中,甚至更好地記錄到遙測工具(如Application Insights)中。
8.測試內存泄漏
主動測試內存泄漏是一個好習慣。這并不難。您可以使用以下簡短模式:
[Test] void MemoryLeakTest() { var weakRef = new WeakReference(leakyObject) // Ryn an operation with leakyObject GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); Assert.IsFalse(weakRef.IsAlive); }為了進行更深入的測試,諸如SciTech的.NET Memory Profiler和dotMemory之類的內存分析器提供了一個測試API:
MemAssertion.NoInstances(typeof(MyLeakyClass)); MemAssertion.NoNewInstances(typeof(MyLeakyClass), lastSnapshot); MemAssertion.MaxNewInstances(typeof(Bitmap), 10);摘要
你的新年決心是怎樣的?我新年的決心是:更好的內存管理。
我希望這篇文章能給您帶來一些價值,如果您訂閱[9]我的博客或在下面發表評論,我將非常樂意。歡迎任何反饋。
References
[1] 難以利用: https://knowledge.ni.com/KnowledgeArticleDetails?id=kA00Z0000019S9cSAE&l=en-IL[2] 說明: https://stackoverflow.com/a/1986486/1229063[3] 5避免C#.NET中的事件造成內存泄漏的技術中,您應該知道: https://michaelscodingspot.com/2018/12/14/5-techniques-to-avoid-memory-leaks-by-events-in-c-net-you-should-know/[4] 問題: https://github.com/dotnet/coreclr/issues/20156[5] 您應該知道的5種避免C#.NET中的事件造成內存泄漏的技術: https://michaelscodingspot.com/2018/12/14/5-techniques-to-avoid-memory-leaks-by-events-in-c-net-you-should-know/[6] 8 .NET中導致內存泄漏的方法: https://michaelscodingspot.com/ways-to-cause-memory-leaks-in-dotnet/[7] Dispose Pattern: https://docs.microsoft.com/en-us/dotnet/standard/garbage-collection/implementing-dispose[8] 其他并發癥: https://www.jetbrains.com/help/dotmemory/Analyzing_GC_Roots.html[9] 訂閱: https://michaelscodingspot.com/subscribe/
總結
以上是生活随笔為你收集整理的wpf绑定treeview 带查找_如何查找,修复和避免C#.NET中内存泄漏的8个最佳实践的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: GO语言笔记4
- 下一篇: 正式进驻1410实验室!