面試官:工作3年,你連 .NET 內(nèi)存泄露都不知道?
原文連接:
https://michaelscodingspot.com/ways-to-cause-memory-leaks-in-dotnet/
任何有經(jīng)驗(yàn)的.NET開(kāi)發(fā)人員都知道,即使.NET應(yīng)用程序具有垃圾回收器,內(nèi)存泄漏始終會(huì)發(fā)生。并不是說(shuō)垃圾回收器有bug,而是我們有多種方法可以(輕松地)導(dǎo)致托管語(yǔ)言的內(nèi)存泄漏。
內(nèi)存泄漏是一個(gè)偷偷摸摸的壞家伙。很長(zhǎng)時(shí)間以來(lái),它們很容易被忽視,而它們也會(huì)慢慢破壞應(yīng)用程序。隨著內(nèi)存泄漏,你的內(nèi)存消耗會(huì)增加,從而導(dǎo)致GC壓力和性能問(wèn)題。最終,程序?qū)⒃诎l(fā)生內(nèi)存不足異常時(shí)崩潰。
在本文中,我們將介紹.NET程序中內(nèi)存泄漏的最常見(jiàn)原因。所有示例均使用C#,但它們與其他語(yǔ)言也相關(guān)。
定義.NET中的內(nèi)存泄漏
在垃圾回收的環(huán)境中,“內(nèi)存泄漏”這個(gè)術(shù)語(yǔ)有點(diǎn)違反直覺(jué)。當(dāng)有一個(gè)垃圾回收器(GC)負(fù)責(zé)收集所有東西時(shí),我的內(nèi)存怎么會(huì)泄漏呢?
這里有兩個(gè)核心原因。第一個(gè)核心原因是你的對(duì)象仍被引用但實(shí)際上卻未被使用。由于它們被引用,因此GC將不會(huì)收集它們,這樣它們將永久保存并占用內(nèi)存。例如,當(dāng)你注冊(cè)了事件但從不注銷(xiāo)時(shí),就有可能會(huì)發(fā)生這種情況。我們稱(chēng)其為托管內(nèi)存泄漏。
第二個(gè)原因是當(dāng)你以某種方式分配非托管內(nèi)存(沒(méi)有垃圾回收)并且不釋放它們。這并不難做到。.NET本身有很多會(huì)分配非托管內(nèi)存的類(lèi)。幾乎所有涉及流、圖形、文件系統(tǒng)或網(wǎng)絡(luò)調(diào)用的操作都會(huì)在背后分配這些非托管內(nèi)存。
通常這些類(lèi)會(huì)實(shí)現(xiàn) Dispose 方法,以釋放內(nèi)存。你自己也可以使用特殊的.NET類(lèi)(如Marshal)或PInvoke輕松地分配非托管內(nèi)存。
許多人都認(rèn)為托管內(nèi)存泄漏根本不是內(nèi)存泄漏,因?yàn)樗鼈內(nèi)匀槐灰茫⑶依碚撋峡梢员换厥?。這是一個(gè)定義問(wèn)題,我的觀點(diǎn)是它們確實(shí)是內(nèi)存泄漏。它們擁有無(wú)法分配給另一個(gè)實(shí)例的內(nèi)存,最終將導(dǎo)致內(nèi)存不足的異常。?
對(duì)于本文,我會(huì)將托管內(nèi)存泄漏和非托管內(nèi)存泄漏都?xì)w為內(nèi)存泄漏。
以下是最常見(jiàn)的8種內(nèi)存泄露的情況。前6個(gè)是托管內(nèi)存泄漏,后2個(gè)是非托管內(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的壽命超過(guò)MyClass,那么你就已經(jīng)造成了內(nèi)存泄漏。wifiManager會(huì)引用MyClass的任何實(shí)例,并且垃圾回收器永遠(yuǎn)不會(huì)回收它們。
Event確實(shí)很危險(xiǎn),我寫(xiě)了整整一篇關(guān)于這個(gè)話題的文章,名為《5 Techniques to avoid Memory Leaks by Events in C# .NET you should know.》
所以,你可以做什么呢?在提到的這篇文章中,有幾種很好的模式可以防止和Event有關(guān)的內(nèi)存泄漏。無(wú)需詳細(xì)說(shuō)明,其中一些是:
注銷(xiāo)訂閱事件。 使用弱句柄(weak-handler)模式。 如果可能,請(qǐng)使用匿名函數(shù)進(jìn)行訂閱,并且不要捕獲任何類(lèi)成員。
2. 在匿名方法中捕獲類(lèi)成員
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?
????????});
????}
}
解決方案可能非常簡(jiǎn)單——分配局部變量:
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)變量
那么什么會(huì)被認(rèn)為是一個(gè)GC Root?
正在運(yùn)行的線程的實(shí)時(shí)堆棧。 靜態(tài)變量。 通過(guò)interop傳遞到COM對(duì)象的托管對(duì)象(內(nèi)存回收將通過(guò)引用計(jì)數(shù)來(lái)完成)。
這意味著靜態(tài)變量及其引用的所有內(nèi)容都不會(huì)被垃圾回收。這里是一個(gè)例子:
public?class?MyClass
{
????static?List?_instances?=?new?List ();
????public?MyClass()
????{
????????_instances.Add(this);
????}
}
4. 緩存功能
的確如此,但是如果無(wú)限期地緩存,最終將耗盡內(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)
????{
????????//?...
????}
}
你可以做一些事情來(lái)解決這個(gè)問(wèn)題:
刪除一段時(shí)間未使用的緩存。 限制緩存大小。 使用WeakReference來(lái)保存緩存的對(duì)象。這依賴(lài)于垃圾收集器來(lái)決定何時(shí)清除緩存,但這可能不是一個(gè)壞主意。GC會(huì)將仍在使用的對(duì)象推廣到更高的世代,以使它們的保存時(shí)間更長(zhǎng)。這意味著經(jīng)常使用的對(duì)象將在緩存中停留更長(zhǎng)時(shí)間。
5. 錯(cuò)誤的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>
這個(gè)View Model將永遠(yuǎn)留在內(nèi)存中:
public?class?MyViewModel
{
????public?string?_someText?=?"memory?leak";
????public?string?SomeText
????{
????????get?{?return?_someText;?}
????????set
????????{
????????????_someText?=?value;
????????}
????}
}
而這個(gè)View Model不會(huì)導(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ā)生的一個(gè)例子是使用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. 沒(méi)有回收非托管內(nèi)存
這里有一個(gè)簡(jiǎn)單的例子。
public?class?SomeClass
{
????private?IntPtr?_buffer;
????public?SomeClass()
????{
????????_buffer?=?Marshal.AllocHGlobal(1000);
????}
????//?do?stuff?without?freeing?the?buffer?memory
}
要解決此類(lèi)問(wèn)題,你可以添加一個(gè)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語(yǔ)句:
using?(var?instance?=?new?MyClass())
{
????//?...?
}
這適用于實(shí)現(xiàn)了IDisposable接口的類(lèi),并且編譯器會(huì)將其轉(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);
????}
}
總結(jié)


一招搞定github下載速度到2MB/s

微信突然更新,這些功能被禁用了
