<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>

          面試官:工作3年,你連 .NET 內(nèi)存泄露都不知道?

          共 7508字,需瀏覽 16分鐘

           ·

          2020-10-07 14:36




          原文連接:

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

          作者 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)匀槐灰茫⑶依碚撋峡梢员换厥?。這是一個(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?

          1. 正在運(yùn)行的線程的實(shí)時(shí)堆棧。
          2. 靜態(tài)變量。
          3. 通過(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);
          ????}
          }

          如果你出于某種原因而決定編寫(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?point
          ????????if?(!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)榧词箳伋霎惓?,也?huì)調(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);
          ????}
          }

          這種模式可確保即使沒(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)容的更多信息。

          希望你喜歡這篇文章,并祝你編程愉快。
          回復(fù)?【關(guān)閉】學(xué)關(guān)
          回復(fù)?【實(shí)戰(zhàn)】獲取20套實(shí)戰(zhàn)源碼
          回復(fù)?【被刪】學(xué)個(gè)
          回復(fù)?【訪客】學(xué)
          回復(fù)?【小程序】學(xué)獲取15套【入門(mén)+實(shí)戰(zhàn)+賺錢(qián)】小程序源碼
          回復(fù)?【python】學(xué)微獲取全套0基礎(chǔ)Python知識(shí)手冊(cè)
          回復(fù)?【2019】獲取2019 .NET 開(kāi)發(fā)者峰會(huì)資料PPT
          回復(fù)?【加群】加入dotnet微信交流群

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


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
































































          瀏覽 49
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <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>
                  囯产精品久久精品 | 91久久精品国自产合 | 色色色色大香蕉 | 色综合网,天天网 | 天天好逼av |