会不会导致内存泄漏_可能会导致.NET内存泄露的8种行为
作者 Michael Shpilt。授權(quán)翻譯,轉(zhuǎn)載請(qǐng)保留原文鏈接。
任何有經(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)匀槐灰?#xff0c;并且理論上可以被回收。 這是一個(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
.NET中的Events因?qū)е聝?nèi)存泄漏而臭名昭著。 原因很簡(jiǎn)單:訂閱事件后,該對(duì)象將保留對(duì)你的類(lèi)的引用。 除非你使用不捕獲類(lèi)成員的匿名方法。 考慮以下示例:
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)成員
雖然可以很明顯地看出事件機(jī)制需要引用一個(gè)對(duì)象,但是引用對(duì)象這個(gè)事情在匿名方法中捕獲類(lèi)成員時(shí)卻不明顯了。
這里是一個(gè)例子:
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 });} }在代碼中,類(lèi)成員_id是在匿名方法中被捕獲的,因此該實(shí)例也會(huì)被引用。 這意味著,盡管JobQueue存在并已經(jīng)引用了job委托,但它還將引用一個(gè)MyClass的實(shí)例。
解決方案可能非常簡(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 });} }通過(guò)將值分配給局部變量,不會(huì)有任何內(nèi)容被捕獲,并且避免了潛在的內(nèi)存泄漏。
3.靜態(tài)變量
我知道有些開(kāi)發(fā)人員認(rèn)為使用靜態(tài)變量始終是一種不好的做法。 盡管有些極端,但在談?wù)搩?nèi)存泄漏時(shí)的確需要注意它。
讓我們考慮一下垃圾收集器的工作原理。 基本思想是GC遍歷所有GC Root對(duì)象并將其標(biāo)記為“不可收集”。 然后,GC轉(zhuǎn)到它們引用的所有對(duì)象,并將它們也標(biāo)記為“不可收集”。 最后,GC收集剩下的所有內(nèi)容。
那么什么會(huì)被認(rèn)為是一個(gè)GC Root?
這意味著靜態(tài)變量及其引用的所有內(nèi)容都不會(huì)被垃圾回收。 這里是一個(gè)例子:
public class MyClass {static List<MyClass> _instances = new List<MyClass>();public MyClass(){_instances.Add(this);} }如果你出于某種原因而決定編寫(xiě)上述代碼,那么任何MyClass的實(shí)例將永遠(yuǎn)留在內(nèi)存中,從而導(dǎo)致內(nèi)存泄漏。
4.緩存功能
開(kāi)發(fā)人員喜歡緩存。 如果一個(gè)操作能只做一次并且將其結(jié)果保存,那么為什么還要做兩次呢?
的確如此,但是如果無(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 pointif (!PictureCache.ContainsKey(id)){var picture = GetPictureFromDatabase(id);PictureCache[id] = picture;}return PictureCache[id];}private byte[] GetPictureFromDatabase(int id){// ...} }這段代碼可能會(huì)節(jié)省一些昂貴的數(shù)據(jù)庫(kù)訪問(wèn)時(shí)間,但是代價(jià)卻是使你的內(nèi)存混亂。
你可以做一些事情來(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綁定
WPF綁定實(shí)際上可能會(huì)導(dǎo)致內(nèi)存泄漏。 經(jīng)驗(yàn)法則是始終綁定到DependencyObject或INotifyPropertyChanged對(duì)象。 如果你不這樣做,WPF將創(chuàng)建從靜態(tài)變量到綁定源(即ViewModel)的強(qiáng)引用,從而導(dǎo)致內(nèi)存泄漏。
這里是一個(gè)例子:
<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)));}}是否調(diào)用PropertyChanged實(shí)際上并不重要,重要的是該類(lèi)是從INotifyPropertyChanged派生的。 因?yàn)檫@會(huì)告訴WPF不要?jiǎng)?chuàng)建強(qiáng)引用。
另一個(gè)和WPF有關(guān)的內(nèi)存泄漏問(wèn)題會(huì)發(fā)生在綁定到集合時(shí)。 如果該集合未實(shí)現(xiàn)INotifyCollectionChanged接口,則會(huì)發(fā)生內(nèi)存泄漏。 你可以通過(guò)使用實(shí)現(xiàn)該接口的ObservableCollection來(lái)避免此問(wèn)題。
6.永不終止的線程
我們已經(jīng)討論過(guò)了GC的工作方式以及GC root。 我提到過(guò)實(shí)時(shí)堆棧會(huì)被視為GC root。 實(shí)時(shí)堆棧包括正在運(yùn)行的線程中的所有局部變量和調(diào)用堆棧的成員。
如果出于某種原因,你要?jiǎng)?chuàng)建一個(gè)永遠(yuǎn)運(yùn)行的不執(zhí)行任何操作并且具有對(duì)對(duì)象引用的線程,那么這將會(huì)導(dǎo)致內(nèi)存泄漏。
這種情況很容易發(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}如果你并沒(méi)有真正的停止這個(gè)timer,那么它會(huì)在一個(gè)單獨(dú)的線程中運(yùn)行,并且由于引用了一個(gè)MyClass的實(shí)例,因此會(huì)阻止該實(shí)例被收集。
7.沒(méi)有回收非托管內(nèi)存
到目前為止,我們僅僅談?wù)摿送泄軆?nèi)存,也就是由垃圾收集器管理的內(nèi)存。 非托管內(nèi)存是完全不同的問(wèn)題,你將需要顯式地回收內(nèi)存,而不僅僅是避免不必要的引用。
這里有一個(gè)簡(jiǎn)單的例子。
public class SomeClass {private IntPtr _buffer;public SomeClass(){_buffer = Marshal.AllocHGlobal(1000);}// do stuff without freeing the buffer memory}在上述方法中,我們使用了Marshal.AllocHGlobal方法,它分配了非托管內(nèi)存緩沖區(qū)。 在這背后,AllocHGlobal會(huì)調(diào)用Kernel32.dll中的LocalAlloc函數(shù)。 如果沒(méi)有使用Marshal.FreeHGlobal顯式地釋放句柄,則該緩沖區(qū)內(nèi)存將被視為占用了進(jìn)程的內(nèi)存堆,從而導(dǎo)致內(nèi)存泄漏。
要解決此類(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);} } 由于內(nèi)存碎片問(wèn)題,非托管內(nèi)存泄漏比托管內(nèi)存泄漏更嚴(yán)重。 垃圾回收器可以移動(dòng)托管內(nèi)存,從而為其他對(duì)象騰出空間。 但是,非托管內(nèi)存將永遠(yuǎn)卡在它的位置。
8.添加了Dispose方法卻不調(diào)用它
在最后一個(gè)示例中,我們添加了Dispose方法以釋放所有非托管資源。 這很棒,但是當(dāng)有人使用了該類(lèi)卻沒(méi)有調(diào)用Dispose時(shí)會(huì)發(fā)生什么呢?
為了避免這種情況,你可以在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(); }這非常有用,因?yàn)榧词箳伋霎惓?#xff0c;也會(huì)調(diào)用Dispose。
你可以做的另一件事是利用Dispose Pattern。 下面的示例演示了這種情況:
public class MyClass : IDisposable {private IntPtr _bufferPtr;public int BUFFER_SIZE = 1024 * 1024; // 1 MBprivate 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);} }這種模式可確保即使沒(méi)有調(diào)用Dispose,Dispose也將在實(shí)例被垃圾回收時(shí)被調(diào)用。 另一方面,如果調(diào)用了Dispose,則finalizer將被抑制(SuppressFinalize)。 抑制finalizer很重要,因?yàn)閒inalizer開(kāi)銷(xiāo)很大并且會(huì)導(dǎo)致性能問(wèn)題。
然而,dispose-pattern不是萬(wàn)無(wú)一失的。 如果從未調(diào)用Dispose并且由于托管內(nèi)存泄漏而導(dǎo)致你的類(lèi)沒(méi)有被垃圾回收,那么非托管資源也將不會(huì)被釋放。
總結(jié)
知道內(nèi)存泄漏是如何發(fā)生的很重要,但只有這些還不夠。 同樣重要的是要認(rèn)識(shí)到現(xiàn)有應(yīng)用程序中存在內(nèi)存泄漏問(wèn)題,找到并修復(fù)它們。 你可以閱讀我的文章《Find, Fix, and Avoid Memory Leaks in C# .NET: 8 Best Practices》,以獲取有關(guān)此內(nèi)容的更多信息。
希望你喜歡這篇文章,并祝你編程愉快。
總結(jié)
以上是生活随笔為你收集整理的会不会导致内存泄漏_可能会导致.NET内存泄露的8种行为的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 内存条选择攻略:锐龙1600x的性能提升
- 下一篇: asp.net ajax控件工具集 Au