保持.NET應(yīng)用程序內(nèi)存健康的六個優(yōu)秀實踐
本文轉(zhuǎn)載自微信公眾號「DotNET技術(shù)圈」,作者michael。轉(zhuǎn)載本文請聯(lián)系DotNET技術(shù)圈公眾號。
大型 .NET 應(yīng)用程序中的內(nèi)存問題是某種無聲的殺手。有點(diǎn)像高血壓。你可以長期吃垃圾食品而忽略它,直到有一天你面臨嚴(yán)重的問題。對于 .NET 程序,該嚴(yán)重問題可能是高內(nèi)存消耗、主要性能問題和徹底崩潰。在這篇文章中,您將看到如何將我們的應(yīng)用程序的血壓保持在健康水平。
你怎么知道你的內(nèi)存使用情況是否健康?你需要做什么來保持它的健康?這正是本文要討論的內(nèi)容。我們將介紹 6 種最佳做法,以保持內(nèi)存健康并在出現(xiàn)問題時檢測問題。您還將看到優(yōu)化垃圾收集并使您的應(yīng)用程序非??焖俚淖罴褜嵺`。
1. 應(yīng)盡快收集對象
為了使您的程序快速運(yùn)行,主要目標(biāo)是盡快收集對象。要理解為什么它很重要,您需要了解 .NET 的分代垃圾收集器。當(dāng)使用該new子句創(chuàng)建對象時,它們是在第 0 代的堆上創(chuàng)建的。那是內(nèi)存中非常小的空間。如果在有 Gen 0 集合時它們?nèi)匀槐灰茫鼈儗⒈惶嵘?Gen 1。Gen 1 是更大的內(nèi)存空間。如果它們在有第 1 代集合時仍被引用,則將它們提升到第 2 代。
Gen 0 集合是最頻繁的并且非???。Gen 1 集合涵蓋 Gen 0 內(nèi)存空間和 Gen 1 內(nèi)存空間,并且它們更昂貴。Gen 2 集合包括整個內(nèi)存空間,包括大對象堆 (LOH)。它們非常昂貴。GC 已優(yōu)化為具有許多 Gen 0 集合、較少的 Gen 1 集合和很少的 Gen 2 集合。但是,如果您有許多對象被提升到更高代,那么您將產(chǎn)生相反的效果。這會導(dǎo)致內(nèi)存壓力[1](又名 GC 壓力)和性能不佳。
順便說一下,新對象的分配非常便宜。您唯一需要擔(dān)心的是集合。
那么如何在低代收集對象呢?很簡單,只需確保不會盡快引用它們即可。有些對象,比如單例,必須永遠(yuǎn)在內(nèi)存中。沒關(guān)系,它們通常是不會消耗大量內(nèi)存的服務(wù)。
2. 使用緩存……但要小心
根據(jù)定義,像緩存這樣的機(jī)制很麻煩。這些是長期存在的臨時對象,可能會升級到第 2 代。雖然這對 GC 壓力不利,但通常值得付出代價,因為緩存確實可以幫助提高性能。但你必須密切關(guān)注它。
緩解部分內(nèi)存壓力的一種方法是使用可變緩存對象。這意味著不是替換緩存對象,而是更新現(xiàn)有對象。這意味著 GC 提升對象和啟動更多 Gen 0 和 Gen 1 收集的工作更少。
這是一個例子。假設(shè)您正在緩存來自在線雜貨店的庫存商品。您有一個緩存機(jī)制來存儲經(jīng)常查詢的項目的價格和數(shù)據(jù)。就像那些會導(dǎo)致高血壓的冷凍比薩餅。假設(shè)每 5 分鐘您必須使緩存無效并重新查詢數(shù)據(jù)庫,以防細(xì)節(jié)發(fā)生變化。因此,在這種情況下,不是創(chuàng)建新Pizza對象,而是更改現(xiàn)有對象的狀態(tài)。
3. 留意 GC 中的時間百分比
如果您想知道垃圾收集對執(zhí)行時間的影響有多大,這很容易做到。簡單看看性能計數(shù)器.NET CLR Memory | % GC 時間。這將顯示垃圾收集器使用了百分之多少的執(zhí)行時間。有許多工具可以查看性能計數(shù)器。在 Windows 中,您可以使用 PerfMon。在 Linux 中,您可以使用dotnet-trace[2]。要了解更多信息,請查看我的文章Use Performance Counters in .NET to measure Memory, CPU, and Everything[3]。
我將給您一些神奇的數(shù)字,但請注意這些數(shù)字,因為一切都有其自身的背景。對于大型應(yīng)用程序,10% 的 GC 時間可能是一個健康的百分比。GC 中 20% 的時間處于臨界狀態(tài),任何更多都意味著您有問題。
4. 留意那些 Gen 2 Collections
除了 GC 中的時間百分比,您應(yīng)該監(jiān)控的另一個重要指標(biāo)是 Gen 2 收集的數(shù)量?;蛘吒_切地說是第 2 代收藏的速度。目標(biāo)是盡可能少地使用它們??紤]到這些是完整的內(nèi)存堆集合。當(dāng) GC 收集所有內(nèi)容時,它們有效地凍結(jié)了應(yīng)用程序的所有線程。
對于您應(yīng)該擁有多少 Gen 2 系列,我無法給出一個神奇的數(shù)字。但我建議每隔一段時間積極監(jiān)控這個數(shù)字,如果比率上升,那么你可能會添加一些非常糟糕的行為。您可以通過性能計數(shù)器.NET CLR Memory |查看該數(shù)字。% Gen 2 集合
PerfMon 顯示第 2 代集合
5. 監(jiān)控穩(wěn)定的內(nèi)存消耗
考慮應(yīng)用程序的常規(guī)狀態(tài)。有些事情一直在發(fā)生。它可能是一個服務(wù)請求的服務(wù)器,一個從隊列中提取消息的服務(wù),一個有很多屏幕的桌面應(yīng)用程序。在此期間,您的應(yīng)用程序不斷創(chuàng)建新對象,執(zhí)行一些操作,然后釋放這些對象并返回到正常狀態(tài)。這意味著從長遠(yuǎn)來看,內(nèi)存消耗應(yīng)該或多或少相同。當(dāng)然,它可能會在高峰時間或繁重操作期間達(dá)到高水平,但一旦完成它應(yīng)該會恢復(fù)正常。
但是,如果您監(jiān)控了許多應(yīng)用程序,您可能知道有時內(nèi)存會隨著時間的推移而增加。內(nèi)存的平均消費(fèi)緩慢上升到更高的水平,即使它在邏輯上不應(yīng)該。這種行為的原因幾乎總是內(nèi)存泄漏[4]。這是一個對象不再使用的現(xiàn)象,但由于某種原因,它仍然被引用,因此從未被收集。
當(dāng)一個操作導(dǎo)致對象泄漏時,每個這樣的操作都會消耗更多的內(nèi)存。隨著時間的推移,記憶力上升。當(dāng)足夠的時間過去時,內(nèi)存接近其極限。在 32 位進(jìn)程中,該限制為 4GB。在 64 位進(jìn)程中,它取決于機(jī)器約束。當(dāng)我們?nèi)绱私咏鼧O限時,垃圾收集器就會恐慌。它開始為每個其他分配觸發(fā)全內(nèi)存第 2 代收集,以免內(nèi)存不足。這很容易使您的應(yīng)用程序變慢。當(dāng)更多的時間過去時,內(nèi)存確實達(dá)到了它的極限,并且應(yīng)用程序會因災(zāi)難性的OutOfMemoryException. 你有它 - 相當(dāng)于心臟病發(fā)作。
為了確保您不會達(dá)到這種狀態(tài),我的建議是隨著時間的推移主動監(jiān)控內(nèi)存消耗。最好的方法是查看性能計數(shù)器Process | Private Bytes。您可以使用Process explorer[5]或 PerfMon輕松完成。
6. 定期查找內(nèi)存泄漏
內(nèi)存問題的#1 罪魁禍?zhǔn)缀翢o疑問是內(nèi)存泄漏。很容易造成它們,它們可以被長期忽視,最終會造成大量損害。在應(yīng)用程序持續(xù)崩潰的階段修復(fù)內(nèi)存泄漏非常困難。您必須更改可能導(dǎo)致各種回歸錯誤的舊代碼。因此,我將為具有健康內(nèi)存的應(yīng)用程序添加第二個主要目標(biāo):修復(fù)并避免內(nèi)存泄漏。
期望您的團(tuán)隊永遠(yuǎn)不會引入內(nèi)存泄漏是不現(xiàn)實的。并且在每次新提交時檢查整個應(yīng)用程序中的內(nèi)存泄漏是不切實際的。相反,我建議添加每隔一段時間檢查內(nèi)存泄漏的做法,它可能是每周、每月或每季度,具體取決于你的需求。
一種方法是在每次看到內(nèi)存上升時檢查內(nèi)存泄漏(如提示 #5 中建議的那樣)。但問題是內(nèi)存占用低的泄漏也會導(dǎo)致很多問題。例如,您可能有一些本應(yīng)收集但仍處于活動狀態(tài)的對象,并且仍有代碼在其中執(zhí)行,這會導(dǎo)致不正確的行為。
檢測和修復(fù)內(nèi)存泄漏的最佳方法是使用內(nèi)存分析器。在我的文章Demystifying Memory Profilers in C# .NET Part 2: Memory Leaks 中[6]了解如何做到這一點(diǎn)。
要了解哪種設(shè)計會導(dǎo)致內(nèi)存泄漏,請查看我的文章.NET 中可能導(dǎo)致內(nèi)存泄漏的 8 種方式[7]。
總結(jié)
所以你有它,一個健康記憶狀態(tài)的秘訣。如果您遵循這些建議,您的應(yīng)用程序?qū)⒑芸觳⑶蚁暮苌俚膬?nèi)存。但說真的,請吃健康的食物和鍛煉??
References
[1] 內(nèi)存壓力: https://michaelscodingspot.com/avoid-gc-pressure/
[2] dotnet-trace: https://github.com/dotnet/diagnostics/blob/master/documentation/dotnet-trace-instructions.md
[3] Use Performance Counters in .NET to measure Memory, CPU, and Everything: https://michaelscodingspot.com/performance-counters/
[4] 內(nèi)存泄漏: https://michaelscodingspot.com/ways-to-cause-memory-leaks-in-dotnet/
[5] Process explorer: https://docs.microsoft.com/en-us/sysinternals/downloads/process-explorer
[6] Demystifying Memory Profilers in C# .NET Part 2: Memory Leaks 中: https://michaelscodingspot.com/memory-profilers-for-memory-leaks/
[7] .NET 中可能導(dǎo)致內(nèi)存泄漏的 8 種方式: https://michaelscodingspot.com/ways-to-cause-memory-leaks-in-dotnet/