.NET 內(nèi)存泄露是否知道?
原文連接:
https://michaelscodingspot.com/ways-to-cause-memory-leaks-in-dotnet/
任何有經(jīng)驗(yàn)的.NET開發(fā)人員都知道,即使.NET應(yīng)用程序具有垃圾回收器,內(nèi)存泄漏始終會發(fā)生。并不是說垃圾回收器有bug,而是我們有多種方法可以(輕松地)導(dǎo)致托管語言的內(nèi)存泄漏。
內(nèi)存泄漏是一個偷偷摸摸的壞家伙。很長時間以來,它們很容易被忽視,而它們也會慢慢破壞應(yīng)用程序。隨著內(nèi)存泄漏,你的內(nèi)存消耗會增加,從而導(dǎo)致GC壓力和性能問題。最終,程序?qū)⒃诎l(fā)生內(nèi)存不足異常時崩潰。
在本文中,我們將介紹.NET程序中內(nèi)存泄漏的最常見原因。所有示例均使用C#,但它們與其他語言也相關(guān)。
定義.NET中的內(nèi)存泄漏
在垃圾回收的環(huán)境中,“內(nèi)存泄漏”這個術(shù)語有點(diǎn)違反直覺。當(dāng)有一個垃圾回收器(GC)負(fù)責(zé)收集所有東西時,我的內(nèi)存怎么會泄漏呢?
這里有兩個核心原因。第一個核心原因是你的對象仍被引用但實(shí)際上卻未被使用。由于它們被引用,因此GC將不會收集它們,這樣它們將永久保存并占用內(nèi)存。例如,當(dāng)你注冊了事件但從不注銷時,就有可能會發(fā)生這種情況。我們稱其為托管內(nèi)存泄漏。
第二個原因是當(dāng)你以某種方式分配非托管內(nèi)存(沒有垃圾回收)并且不釋放它們。這并不難做到。.NET本身有很多會分配非托管內(nèi)存的類。幾乎所有涉及流、圖形、文件系統(tǒng)或網(wǎng)絡(luò)調(diào)用的操作都會在背后分配這些非托管內(nèi)存。
通常這些類會實(shí)現(xiàn) Dispose 方法,以釋放內(nèi)存。你自己也可以使用特殊的.NET類(如Marshal)或PInvoke輕松地分配非托管內(nèi)存。
許多人都認(rèn)為托管內(nèi)存泄漏根本不是內(nèi)存泄漏,因?yàn)樗鼈內(nèi)匀槐灰茫⑶依碚撋峡梢员换厥铡_@是一個定義問題,我的觀點(diǎn)是它們確實(shí)是內(nèi)存泄漏。它們擁有無法分配給另一個實(shí)例的內(nèi)存,最終將導(dǎo)致內(nèi)存不足的異常。?
對于本文,我會將托管內(nèi)存泄漏和非托管內(nèi)存泄漏都?xì)w為內(nèi)存泄漏。
以下是最常見的8種內(nèi)存泄露的情況。前6個是托管內(nèi)存泄漏,后2個是非托管內(nèi)存泄漏:
1. 訂閱Events
public?class?MyClass
{
????public?MyClass(WiFiManager?wiFiManager)
????{
????????wiFiManager.WiFiSignalChanged?+=?OnWiFiChanged;
????}
????private?void?OnWiFiChanged(object?sender,?WifiEventArgs?e)
????{
????????//?do?something
????}
}
假設(shè)wifiManager的壽命超過MyClass,那么你就已經(jīng)造成了內(nèi)存泄漏。wifiManager會引用MyClass的任何實(shí)例,并且垃圾回收器永遠(yuǎn)不會回收它們。
Event確實(shí)很危險,我寫了整整一篇關(guān)于這個話題的文章,名為《5 Techniques to avoid Memory Leaks by Events in C# .NET you should know.》
所以,你可以做什么呢?在提到的這篇文章中,有幾種很好的模式可以防止和Event有關(guān)的內(nèi)存泄漏。無需詳細(xì)說明,其中一些是:
注銷訂閱事件。 使用弱句柄(weak-handler)模式。 如果可能,請使用匿名函數(shù)進(jìn)行訂閱,并且不要捕獲任何類成員。
2. 在匿名方法中捕獲類成員
public?class?MyClass
{
????private?JobQueue?_jobQueue;
????private?int?_id;
????public?MyClass(JobQueue?jobQueue)
????{
????????_jobQueue?=?jobQueue;
????}
????public?void?Foo()
????{
????????_jobQueue.EnqueueJob(()?=>
????????{
????????????Logger.Log($"Executing?job?with?ID?{_id}");
????????????//?do?stuff?
????????});
????}
}
解決方案可能非常簡單——分配局部變量:
public?class?MyClass
{
????public?MyClass(JobQueue?jobQueue)
????{
????????_jobQueue?=?jobQueue;
????}
????private?JobQueue?_jobQueue;
????private?int?_id;
????public?void?Foo()
????{
????????var?localId?=?_id;
????????_jobQueue.EnqueueJob(()?=>
????????{
????????????Logger.Log($"Executing?job?with?ID?{localId}");
????????????//?do?stuff?
????????});
????}
}
3. 靜態(tài)變量
那么什么會被認(rèn)為是一個GC Root?
正在運(yùn)行的線程的實(shí)時堆棧。 靜態(tài)變量。 通過interop傳遞到COM對象的托管對象(內(nèi)存回收將通過引用計數(shù)來完成)。
這意味著靜態(tài)變量及其引用的所有內(nèi)容都不會被垃圾回收。這里是一個例子:
public?class?MyClass
{
????static?List?_instances?=?new?List ();
????public?MyClass()
????{
????????_instances.Add(this);
????}
}
4. 緩存功能
的確如此,但是如果無限期地緩存,最終將耗盡內(nèi)存。考慮以下示例:
public?class?ProfilePicExtractor
{
????private?Dictionary<int,?byte[]>?PictureCache?{?get;?set;?}?=?
??????new?Dictionary<int,?byte[]>();
????public?byte[]?GetProfilePicByID(int?id)
????{
????????//?A?lock?mechanism?should?be?added?here,?but?let s?stay?on?point
????????if?(!PictureCache.ContainsKey(id))
????????{
????????????var?picture?=?GetPictureFromDatabase(id);
????????????PictureCache[id]?=?picture;
????????}
????????return?PictureCache[id];
????}
????private?byte[]?GetPictureFromDatabase(int?id)
????{
????????//?...
????}
}
你可以做一些事情來解決這個問題:
刪除一段時間未使用的緩存。 限制緩存大小。 使用WeakReference來保存緩存的對象。這依賴于垃圾收集器來決定何時清除緩存,但這可能不是一個壞主意。GC會將仍在使用的對象推廣到更高的世代,以使它們的保存時間更長。這意味著經(jīng)常使用的對象將在緩存中停留更長時間。
5. 錯誤的WPF綁定
<UserControl?x:Class="WpfApp.MyControl"
?????????????xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
?????????????xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
????<TextBlock?Text="{Binding?SomeText}">TextBlock>
UserControl>
這個View Model將永遠(yuǎn)留在內(nèi)存中:
public?class?MyViewModel
{
????public?string?_someText?=?"memory?leak";
????public?string?SomeText
????{
????????get?{?return?_someText;?}
????????set
????????{
????????????_someText?=?value;
????????}
????}
}
而這個View Model不會導(dǎo)致內(nèi)存泄漏:
public?class?MyViewModel?:?INotifyPropertyChanged
{
????public?string?_someText?=?"not?a?memory?leak";
????public?string?SomeText
????{
????????get?{?return?_someText;?}
????????set
????????{
????????????_someText?=?value;
????????????PropertyChanged?.Invoke(this,?new?PropertyChangedEventArgs(nameof?(SomeText)));
????????}
????}
}
6. 永不終止的線程
這種情況很容易發(fā)生的一個例子是使用Timer。考慮以下代碼:
public?class?MyClass
{
????public?MyClass()
????{
????????Timer?timer?=?new?Timer(HandleTick);
????????timer.Change(TimeSpan.FromSeconds(5),?TimeSpan.FromSeconds(5));
????}
????private?void?HandleTick(object?state)
????{
????????//?do?something
????}
}
7. 沒有回收非托管內(nèi)存
這里有一個簡單的例子。
public?class?SomeClass
{
????private?IntPtr?_buffer;
????public?SomeClass()
????{
????????_buffer?=?Marshal.AllocHGlobal(1000);
????}
????//?do?stuff?without?freeing?the?buffer?memory
}
要解決此類問題,你可以添加一個Dispose方法,以釋放所有非托管資源,如下所示:
public?class?SomeClass?:?IDisposable
{
????private?IntPtr?_buffer;
????public?SomeClass()
????{
????????_buffer?=?Marshal.AllocHGlobal(1000);
????????//?do?stuff?without?freeing?the?buffer?memory
????}
????public?void?Dispose()
????{
????????Marshal.FreeHGlobal(_buffer);
????}
}
8. 添加了Dispose方法卻不調(diào)用它
為了避免這種情況,你可以在C#中使用using語句:
using?(var?instance?=?new?MyClass())
{
????//?...?
}
這適用于實(shí)現(xiàn)了IDisposable接口的類,并且編譯器會將其轉(zhuǎn)化為下面的形式:
MyClass?instance?=?new?MyClass();;
try
{
????//?...
}
finally
{
????if?(instance?!=?null)
????????((IDisposable)instance).Dispose();
}
你可以做的另一件事是利用Dispose Pattern。下面的示例演示了這種情況:
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);
????}
}
