<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          .NET 內(nèi)存泄露是否知道?

          共 7290字,需瀏覽 15分鐘

           ·

          2020-10-10 22:57

          原文連接:

          https://michaelscodingspot.com/ways-to-cause-memory-leaks-in-dotnet/

          作者 Michael Shpilt。授權(quán)翻譯,轉(zhuǎn)載請保留原文鏈接。


          任何有經(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

          .NET中的Events因?qū)е聝?nèi)存泄漏而臭名昭著。原因很簡單:訂閱事件后,該對象將保留對你的類的引用。除非你使用不捕獲類成員的匿名方法。考慮以下示例:
          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. 在匿名方法中捕獲類成員

          雖然可以很明顯地看出事件機(jī)制需要引用一個對象,但是引用對象這個事情在匿名方法中捕獲類成員時卻不明顯了。

          這里是一個例子:
          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?
          ????????});
          ????}
          }

          在代碼中,類成員_id是在匿名方法中被捕獲的,因此該實(shí)例也會被引用。這意味著,盡管JobQueue存在并已經(jīng)引用了job委托,但它還將引用一個MyClass的實(shí)例。

          解決方案可能非常簡單——分配局部變量:

          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?
          ????????});
          ????}
          }

          通過將值分配給局部變量,不會有任何內(nèi)容被捕獲,并且避免了潛在的內(nèi)存泄漏。

          3. 靜態(tài)變量

          我知道有些開發(fā)人員認(rèn)為使用靜態(tài)變量始終是一種不好的做法。盡管有些極端,但在談?wù)搩?nèi)存泄漏時的確需要注意它。

          讓我們考慮一下垃圾收集器的工作原理。基本思想是GC遍歷所有GC Root對象并將其標(biāo)記為“不可收集”。然后,GC轉(zhuǎn)到它們引用的所有對象,并將它們也標(biāo)記為“不可收集”。最后,GC收集剩下的所有內(nèi)容。

          那么什么會被認(rèn)為是一個GC Root?

          1. 正在運(yùn)行的線程的實(shí)時堆棧。
          2. 靜態(tài)變量。
          3. 通過interop傳遞到COM對象的托管對象(內(nèi)存回收將通過引用計數(shù)來完成)。


          這意味著靜態(tài)變量及其引用的所有內(nèi)容都不會被垃圾回收。這里是一個例子:

          public?class?MyClass
          {

          ????static?List?_instances?=?new?List();
          ????public?MyClass()
          ????
          {
          ????????_instances.Add(this);
          ????}
          }

          如果你出于某種原因而決定編寫上述代碼,那么任何MyClass的實(shí)例將永遠(yuǎn)留在內(nèi)存中,從而導(dǎo)致內(nèi)存泄漏。

          4. 緩存功能

          開發(fā)人員喜歡緩存。如果一個操作能只做一次并且將其結(jié)果保存,那么為什么還要做兩次呢?

          的確如此,但是如果無限期地緩存,最終將耗盡內(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)
          ????
          {
          ????????//?...
          ????}
          }

          這段代碼可能會節(jié)省一些昂貴的數(shù)據(jù)庫訪問時間,但是代價卻是使你的內(nèi)存混亂。

          你可以做一些事情來解決這個問題:

          • 刪除一段時間未使用的緩存。
          • 限制緩存大小。
          • 使用WeakReference來保存緩存的對象。這依賴于垃圾收集器來決定何時清除緩存,但這可能不是一個壞主意。GC會將仍在使用的對象推廣到更高的世代,以使它們的保存時間更長。這意味著經(jīng)常使用的對象將在緩存中停留更長時間。


          5. 錯誤的WPF綁定

          WPF綁定實(shí)際上可能會導(dǎo)致內(nèi)存泄漏。經(jīng)驗(yàn)法則是始終綁定到DependencyObject或INotifyPropertyChanged對象。如果你不這樣做,WPF將創(chuàng)建從靜態(tài)變量到綁定源(即ViewModel)的強(qiáng)引用,從而導(dǎo)致內(nèi)存泄漏。

          這里是一個例子:
          <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)));
          ????????}
          ????}
          }

          是否調(diào)用PropertyChanged實(shí)際上并不重要,重要的是該類是從INotifyPropertyChanged派生的。因?yàn)檫@會告訴WPF不要創(chuàng)建強(qiáng)引用。

          另一個和WPF有關(guān)的內(nèi)存泄漏問題會發(fā)生在綁定到集合時。如果該集合未實(shí)現(xiàn)INotifyCollectionChanged接口,則會發(fā)生內(nèi)存泄漏。你可以通過使用實(shí)現(xiàn)該接口的ObservableCollection來避免此問題。

          6. 永不終止的線程

          我們已經(jīng)討論過了GC的工作方式以及GC root。我提到過實(shí)時堆棧會被視為GC root。實(shí)時堆棧包括正在運(yùn)行的線程中的所有局部變量和調(diào)用堆棧的成員。

          如果出于某種原因,你要創(chuàng)建一個永遠(yuǎn)運(yùn)行的不執(zhí)行任何操作并且具有對對象引用的線程,那么這將會導(dǎo)致內(nèi)存泄漏。

          這種情況很容易發(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
          ????}
          }

          如果你并沒有真正的停止這個timer,那么它會在一個單獨(dú)的線程中運(yùn)行,并且由于引用了一個MyClass的實(shí)例,因此會阻止該實(shí)例被收集。

          7. 沒有回收非托管內(nèi)存

          到目前為止,我們僅僅談?wù)摿送泄軆?nèi)存,也就是由垃圾收集器管理的內(nèi)存。非托管內(nèi)存是完全不同的問題,你將需要顯式地回收內(nèi)存,而不僅僅是避免不必要的引用。

          這里有一個簡單的例子。

          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會調(diào)用Kernel32.dll中的LocalAlloc函數(shù)。如果沒有使用Marshal.FreeHGlobal顯式地釋放句柄,則該緩沖區(qū)內(nèi)存將被視為占用了進(jìn)程的內(nèi)存堆,從而導(dǎo)致內(nèi)存泄漏。

          要解決此類問題,你可以添加一個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)存碎片問題,非托管內(nèi)存泄漏比托管內(nèi)存泄漏更嚴(yán)重。垃圾回收器可以移動托管內(nèi)存,從而為其他對象騰出空間。但是,非托管內(nèi)存將永遠(yuǎn)卡在它的位置。
          ?

          8. 添加了Dispose方法卻不調(diào)用它

          在最后一個示例中,我們添加了Dispose方法以釋放所有非托管資源。這很棒,但是當(dāng)有人使用了該類卻沒有調(diào)用Dispose時會發(fā)生什么呢?

          為了避免這種情況,你可以在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();
          }

          這非常有用,因?yàn)榧词箳伋霎惓#矔{(diào)用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);
          ????}
          }

          這種模式可確保即使沒有調(diào)用Dispose,Dispose也將在實(shí)例被垃圾回收時被調(diào)用。另一方面,如果調(diào)用了Dispose,則finalizer將被抑制(SuppressFinalize)。抑制finalizer很重要,因?yàn)閒inalizer開銷很大并且會導(dǎo)致性能問題。

          然而,dispose-pattern不是萬無一失的。如果從未調(diào)用Dispose并且由于托管內(nèi)存泄漏而導(dǎo)致你的類沒有被垃圾回收,那么非托管資源也將不會被釋放。

          總結(jié)

          知道內(nèi)存泄漏是如何發(fā)生的很重要,但只有這些還不夠。同樣重要的是要認(rèn)識到現(xiàn)有應(yīng)用程序中存在內(nèi)存泄漏問題,找到并修復(fù)它們。你可以閱讀我的文章《Find, Fix, and Avoid Memory Leaks in C# .NET: 8 Best Practices》,以獲取有關(guān)此內(nèi)容的更多信息。

          希望你喜歡這篇文章,并祝你編程愉快。


          瀏覽 55
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  人人操人人射人人色 | 中文字幕在线观看第一页2019 | 亚洲人成网77777色在线播放 | 亚洲看逼 | 国产男女网站 |