日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪(fǎng)問(wèn) 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 编程资源 > 编程问答 >内容正文

编程问答

演义群侠传(七)【GC垃圾回收】

發(fā)布時(shí)間:2025/4/5 编程问答 19 豆豆
生活随笔 收集整理的這篇文章主要介紹了 演义群侠传(七)【GC垃圾回收】 小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

在《給AS程序員的一點(diǎn)建議一文》中我提到了釋放資源的重要性。最近在一些項(xiàng)目過(guò)程中我又對(duì)這方面有了更多的理解,在此希望能夠分享給大家。首先讓我們來(lái)回顧一下關(guān)于垃圾回收(Garbage Collection,下文簡(jiǎn)稱(chēng)GC)的一些知識(shí)。要閱讀本文,你需要對(duì)GC機(jī)制有些基本認(rèn)識(shí)。

在ActionScript中,我們沒(méi)有API可以直接刪除一個(gè)對(duì)象,也不能控制Player進(jìn)行GC。但是GC的行為是可以預(yù)估的,作為開(kāi)發(fā)者,我們需要了解的是GC執(zhí)行的時(shí)機(jī)是發(fā)生在需要向操作系統(tǒng)請(qǐng)求分配內(nèi)存的時(shí)候。

從上面的模擬圖我們可以看到:

  • Player以塊的方式請(qǐng)求和釋放內(nèi)存。GC的結(jié)果不一定就是更少的內(nèi)存占用,也有可能是從操作系統(tǒng)獲得更多的可用內(nèi)存。
  • Player會(huì)在某些GC過(guò)程中把內(nèi)存中未使用部分組合成可以釋放的塊還給操作系統(tǒng)。
  • 此外還要注意的是Player為了避免占用太多的CPU資源,會(huì)將一些GC操作分到不同的時(shí)間片中運(yùn)行,所以一次GC過(guò)程并不一定清理完所有可回收資源。

一次GC過(guò)程(GC Pass)分為以下兩個(gè)步驟:

Reference Counting

統(tǒng)計(jì)所有對(duì)象的引用計(jì)數(shù),如果某個(gè)對(duì)象沒(méi)有任何引用,就標(biāo)記為可回收。

這個(gè)操作很好理解,需要強(qiáng)調(diào)的是weak reference(弱引用)是不參與計(jì)算的。引用計(jì)數(shù)是一個(gè)相對(duì)省CPU的操作,能夠篩選出大部分可回收資源,但是對(duì)一些循環(huán)引用的情況就無(wú)能為力了。在下圖中,標(biāo)記為綠色的對(duì)象每個(gè)的引用數(shù)都為1,但它們明顯是應(yīng)該被回收的。

所以GC需要進(jìn)行第二個(gè)步驟:

Mark Sweeping

這個(gè)步驟是從根對(duì)象(Root)開(kāi)始輪詢(xún)對(duì)象的引用。所謂的根對(duì)象包括:

  • Stage對(duì)象
  • 靜態(tài)變量
  • 局部變量

這種方式足夠精確,能夠成功篩選出上圖中綠色標(biāo)記的對(duì)象,而它的代價(jià)就是較大的計(jì)算開(kāi)銷(xiāo)。

為了幫助GC過(guò)程更高效的執(zhí)行,最好是能在第一步引用計(jì)數(shù)中就把需要回收的對(duì)象都標(biāo)記出來(lái)。具體的做法就是把所有不需要的對(duì)象引用全部清空,包括:

  • 刪除成員變量的引用
  • 從可視對(duì)象列表上移除對(duì)象
  • 移除事件監(jiān)聽(tīng)

難點(diǎn):事件監(jiān)聽(tīng)是否會(huì)造成對(duì)象不能回收?這個(gè)問(wèn)題要具體分析,有些情況可以,有些情況卻不可以。歸根結(jié)底還是引用關(guān)系的問(wèn)題。來(lái)看下面這個(gè)例子:

  • Foo.as:
  • public class Foo extends Sprite
  • {
  • private var bar:Sprite;
  • public function Foo()
  • {
  • bar = new Sprite();
  • //...
  • //監(jiān)聽(tīng)bar發(fā)出的事件??梢钥醋魇莃ar引用了foo,因?yàn)閒oo的引用被保存在bar的監(jiān)聽(tīng)者數(shù)組里。
  • bar.addEventListener(MouseEvent.CLICK, clickHandler);
  • addChild(bar);
  • }
  • private function clickHandler(event:MouseEvent):void
  • {
  • ...
  • }
  • }
  • Main.as:
  • // 創(chuàng)建foo實(shí)例
  • var foo:Foo = new Foo();
  • addChild(foo);
  • // do something with foo...
  • // 清除foo的引用
  • removeChild(foo);
  • foo = null;
  • 我們看到foo引用了bar,而bar又通過(guò)事件監(jiān)聽(tīng)的聯(lián)系引用了foo,這就構(gòu)成了一個(gè)循環(huán)引用。根據(jù)前文對(duì)GC步驟的分析,這兩個(gè)對(duì)象都必須到第二步mark and sweep才能被標(biāo)記出來(lái)。

    如果我們對(duì)事件監(jiān)聽(tīng)用弱引用的方式:

  • bar.addEventListener(MouseEvent.CLICK, clickHandler, false, 0, true);
  • 由于弱引用不計(jì)入引用計(jì)數(shù),所以現(xiàn)在foo的引用數(shù)為0。GC在第一步操作中就能把foo標(biāo)記出來(lái),從而減少了一些運(yùn)算開(kāi)銷(xiāo)。這也是為什么Grant Skinner呼吁把弱引用作為事件監(jiān)聽(tīng)的默認(rèn)方式的原因。

    但是作為最佳實(shí)踐,我們還是提倡要手動(dòng)移除事件監(jiān)聽(tīng)。以下代碼添加一個(gè)destroy()方法(也有習(xí)慣命名為dispose()或kill()等)到Foo對(duì)象中:

  • public function destroy():void
  • {
  • bar.removeEventListener(MouseEvent.CLICK, clickHandler);
  • removeChild(bar);
  • bar = null;
  • }
  • 在清除foo的引用之前執(zhí)行destroy()方法:

  • foo.destroy();
  • removeChild(foo);
  • foo = null;
  • 經(jīng)過(guò)我們?nèi)绱颂幚?#xff0c;兩個(gè)對(duì)象的引用數(shù)全都為0。GC只需第一步處理就完成任務(wù),我們節(jié)約了更多的運(yùn)算開(kāi)銷(xiāo)!

    從上例可以看出在一般情況下,監(jiān)聽(tīng)子對(duì)象的事件不會(huì)影響GC。不管是弱引用方式的監(jiān)聽(tīng),還是顯式移除監(jiān)聽(tīng),都只是幫助GC更高效運(yùn)行的手段,而不會(huì)影響GC的結(jié)果。但是如果事件監(jiān)聽(tīng)造成的是對(duì)象以外的引用關(guān)系,情況就不同了,并且很有可能造成回收失敗。一個(gè)常見(jiàn)的錯(cuò)誤例子是監(jiān)聽(tīng)Stage對(duì)象的RESIZE事件,如果沒(méi)有顯式移除這個(gè)監(jiān)聽(tīng)或者是沒(méi)有采用弱引用方式,那么這個(gè)對(duì)象就不會(huì)被GC回收的。所以我建議大家還是要盡可能的顯式移除監(jiān)聽(tīng),切斷引用關(guān)系。

    我們現(xiàn)在已經(jīng)用對(duì)GC最友好的方式做好了清理準(zhǔn)備,但是對(duì)象還沒(méi)有從內(nèi)存中刪除。在等待GC執(zhí)行的這段時(shí)間,對(duì)象內(nèi)的代碼還在繼續(xù)執(zhí)行。比如加在對(duì)象上的ENTER_FRAME事件監(jiān)聽(tīng)處理還在繼續(xù)執(zhí)行,對(duì)象內(nèi)的MovieClip或Sound都還在繼續(xù)播放。我們一定要避免這種情況的發(fā)生,所以在切斷引用之前,我們還要在destroy()方法中做些清理工作。我們要做的工作包括:

    • clearInterval(),clearTimeout()
    • timer.stop()
    • loader.unload()/loader.unloadAndStop()
    • movieclip.stop() 如果有子MC的,也要停止播放
    • bitmapData.dispose()
    • 關(guān)閉LocalConnection,NetConnection,NetStream
    • 停止音頻和視頻的播放
    • 刪除Camera和Microphone對(duì)象的引用
    • 調(diào)用子對(duì)象的destroy()方法,如果有的話(huà)

    其實(shí)這些都是在開(kāi)發(fā)中管理資源的基本常識(shí),歸結(jié)為一句話(huà)就是:誰(shuí)創(chuàng)建了對(duì)象,誰(shuí)就要負(fù)責(zé)清理該對(duì)象。

    下面就以一些我在實(shí)際項(xiàng)目中開(kāi)發(fā)的destroy()方法為例,看代碼說(shuō)話(huà):

  • public function destroy():void
  • {
  • // List是一個(gè)繼承自Sprite的自定義子類(lèi)
  • // 移除list的事件監(jiān)聽(tīng)
  • list.removeEventListener.remove(MouseEvent.CLICK, clickHandler);
  • // 我們創(chuàng)建了list實(shí)例,也要負(fù)責(zé)清理
  • // 調(diào)用list對(duì)象自己的destroy()方法
  • list.destroy();
  • // 將list從顯示列表移除
  • // 這一步并非必須的步驟,原因是list的父對(duì)象會(huì)被移除,這樣list和stage就沒(méi)有聯(lián)系了。
  • // 但是在我的代碼中,list還有可能會(huì)被添加到外部的容器中(比如stage),那么這一步就是必須的了。
  • list.parent.removeChild(list);
  • list = null;
  • // --------------------
  • // loader是一個(gè)來(lái)自tweenmax類(lèi)庫(kù)中的ImageLoader實(shí)例
  • // 如果loader已經(jīng)創(chuàng)建
  • if(loader)
  • {
  • // 調(diào)用loader的dispose()方法,優(yōu)秀的第三方類(lèi)庫(kù)都應(yīng)該有良好的資源管理機(jī)制。
  • // 參數(shù)true表示把加載的內(nèi)容從顯示列表上移除,幫我們節(jié)約了代碼。
  • loader.dispose(true);
  • loader = null;
  • }
  • // --------------------
  • // lightbox實(shí)例變量保存了一個(gè)外部引用
  • // 根據(jù)誰(shuí)創(chuàng)建誰(shuí)清理的原則,我們?cè)谶@里不需要負(fù)責(zé)該對(duì)象的清理,只要?jiǎng)h除引用就可以了。
  • lightbox = null;
  • }
  • 另一個(gè)示例的destroy()方法演示了對(duì)數(shù)組中對(duì)象的處理方法:

  • public function destroy():void
  • {
  • //?cells是一個(gè)數(shù)組,包含了一組子對(duì)象
  • var i : int, n : int;
  • n = cells.length;
  • for(i = 0; i < n; i++)
  • {
  • // 執(zhí)行每一個(gè)子對(duì)象的destroy()方法
  • cells[i].destroy();
  • // 刪除子對(duì)象的引用
  • cells[i] = null;
  • }
  • // 刪除數(shù)組自身的引用
  • cells = null;
  • // 如果不需要得到每一個(gè)子對(duì)象的引用,我們也可以簡(jiǎn)單的用以下代碼來(lái)清理數(shù)組:
  • //cells.length = 0;
  • //cells = null;
  • }
  • 在結(jié)構(gòu)更復(fù)雜的項(xiàng)目里,我們還可以抽象出一個(gè)IDestroyable接口,讓需要執(zhí)行清理的自定義對(duì)象實(shí)現(xiàn)這個(gè)接口。這樣我們的清理代碼可以寫(xiě)為:

  • var n:int = this.numChildren;
  • for(var i:int = n-1; i >= 0; i--)
  • {
  • if(this.getChildAt(i) is IDestroyable)
  • {
  • IDestroyable(this.getChildAt(i)).destroy();
  • this.removeChildAt(i);
  • }
  • }
  • 總結(jié):GC好比是ActionScript城市的環(huán)衛(wèi)工人,我們的每個(gè)類(lèi)都是從事勞動(dòng)生產(chǎn)的市民。優(yōu)秀的市民會(huì)把生產(chǎn)垃圾分類(lèi)安放到回收點(diǎn),而不文明的市民則把垃圾丟得到處都是。你說(shuō)那種做法讓城市的清掃工作變得更加高效?所以請(qǐng)大家謹(jǐn)記“誰(shuí)創(chuàng)建誰(shuí)清理”的原則,做一位ActionScript好市民。

    本文回顧了GC的一些基本知識(shí)和最佳實(shí)踐。在下一篇中我將結(jié)合一些具體問(wèn)題,為大家把脈GC的疑難雜癥。敬請(qǐng)期待。

    繼續(xù)閱讀:

    ?

    前文我們介紹了GC的工作機(jī)制和幫助GC更好工作的最佳實(shí)踐。其實(shí)只要我們遵守誰(shuí)創(chuàng)建誰(shuí)清理的原則來(lái)管理對(duì)象,就能基本上避免回收失敗,也就是我們通常說(shuō)的內(nèi)存泄漏問(wèn)題。但是在實(shí)際項(xiàng)目中我們還會(huì)看到各種原因引起的內(nèi)存泄漏,接下來(lái)就讓我們一起來(lái)找出病因。

    ?

    首先我們需要觀察癥狀,也就是內(nèi)存的使用曲線(xiàn)。排查的方法是反復(fù)執(zhí)行一些創(chuàng)建和刪除對(duì)象的方法、反復(fù)加載和卸載子文件。如果內(nèi)存曲線(xiàn)一路飆升、或者是居高不下,都表明發(fā)生了內(nèi)存泄漏問(wèn)題。觀察內(nèi)存占用可以直接求助于操作系統(tǒng)的資源管理器,也可以用Hi-ReS-Stats這個(gè)類(lèi)。

    ?

    ?

    第二個(gè)需要觀察的地方,是Player輸出的load和unload信息。加載和卸載外部文件,是內(nèi)存泄漏問(wèn)題的重災(zāi)區(qū)。在調(diào)試階段,我一般會(huì)在主文件加一個(gè)執(zhí)行System.gc()語(yǔ)句的按鈕。一旦卸載了一個(gè)子文件,就手動(dòng)觸發(fā)若干次GC。如果沒(méi)有輸出子文件的卸載信息,那么就說(shuō)明出現(xiàn)泄漏了。

    ?

    ?

    第三個(gè)可以幫助我們排查問(wèn)題的地方是Profiler工具,當(dāng)你刪除了對(duì)象引用,并手動(dòng)觸發(fā)GC以后,可以觀察這個(gè)對(duì)象是否還存在內(nèi)存中。Profiler可以說(shuō)是排查內(nèi)存泄漏問(wèn)題的終極工具,唯一的問(wèn)題就是會(huì)拖慢整體的運(yùn)行速度,比較慢。

    ?

    ?

    觀察到問(wèn)題現(xiàn)象以后我們得順藤摸瓜,找出到底是那個(gè)對(duì)象占著內(nèi)存不放,然后對(duì)癥下藥。下面我們就來(lái)分析幾個(gè)內(nèi)存泄漏的疑難雜癥。

    ?

    病例一:小心loaderContext和applicationDomain

    ?

    ActionScript 3的Loader對(duì)象遠(yuǎn)沒(méi)有我們想象中那么簡(jiǎn)單,內(nèi)存泄漏問(wèn)題有很大一部分是由于不當(dāng)?shù)募虞d和卸載操作引起的。我在研究Gaia框架的內(nèi)存泄漏問(wèn)題的時(shí)候發(fā)現(xiàn)了一處由于沒(méi)有刪除LoaderContext的引用而造成的卸載失敗問(wèn)題,其實(shí)就是沒(méi)有釋放應(yīng)用程序域所造成的。應(yīng)用程序域是一個(gè)需要被重視的對(duì)象,它對(duì)加載和卸載的影響有如下兩點(diǎn):

    ?

    • 如果子SWF文件是加載到主應(yīng)用程序域里的,那么這個(gè)文件是不能卸載的(前提是子SWF文件內(nèi)的類(lèi)定義沒(méi)有被主應(yīng)用程序域里定義所覆蓋)。
    • 如果子SWF文件是加載到子應(yīng)用程序域內(nèi)(Loader的默認(rèn)方式),那么這個(gè)文件是一定能夠被卸載的。

    ?

    關(guān)于應(yīng)用程序域的知識(shí)可以看我以前翻譯的文章。根據(jù)類(lèi)定義在主應(yīng)用程序域里的向下覆蓋原則,我們還可以考慮以下情況:如果再次加載相同的子SWF文件到主應(yīng)用程序域,子文件里所包含的類(lèi)定義將全部忽略,并不會(huì)注冊(cè)到主應(yīng)用程序域中。這次加載的SWF文件則是可以被卸載的。換句話(huà)說(shuō),一旦類(lèi)定義被加入到主應(yīng)用程序域里就不能夠被刪除。而沒(méi)有加載到主應(yīng)用程序域內(nèi)的對(duì)象如果不能卸載就肯定是內(nèi)存泄漏。

    ?

    實(shí)際開(kāi)發(fā)中除了一些確實(shí)不需要卸載的模塊代碼需要加載到主應(yīng)用程序域中,一般我們還是將對(duì)象加載到子應(yīng)用程序域中去的。

    ?

    病例二:小心靜態(tài)類(lèi)

    ?

    癥狀還是某個(gè)子文件加載后不能卸載,但是當(dāng)我們?cè)俅渭虞d這個(gè)子文件的時(shí)候,能從log看到之前的子文件被釋放了。這是一個(gè)輕度內(nèi)存泄漏的例子,一般不會(huì)引起內(nèi)存飆升直到引起crash等強(qiáng)烈后果,但是我們也不能掉以輕心。

    ?

    根據(jù)之前的經(jīng)驗(yàn):不能卸載一定是某個(gè)對(duì)象被占住了,后續(xù)再次加載又能卸載之前的實(shí)例,說(shuō)明前面文件中被占住的資源又被釋放了。我們先通過(guò)Profiler查看到底是那個(gè)對(duì)象被占住,然而分析下來(lái)看到居然是子文件中創(chuàng)建的所有實(shí)例都已經(jīng)釋放了。那么,到底是什么原因呢?

    ?

    既然實(shí)例都已經(jīng)被釋放了,那么只有可能是類(lèi)定義被占住了。我在這個(gè)子文件中用到了Greensock類(lèi)庫(kù)的ImageLoader。通過(guò)研究它的源碼發(fā)現(xiàn)這個(gè)加載類(lèi)庫(kù)采用了與TweenMax類(lèi)似的插件機(jī)制。當(dāng)我第一次引用ImageLoader定義的時(shí)候,它會(huì)自動(dòng)向LoaderMax類(lèi)注冊(cè)。也就是說(shuō)LoaderMax類(lèi)的靜態(tài)成員持有ImageLoader定義的引用。

    ?

    如果這兩個(gè)類(lèi)定義都在子應(yīng)用程序域中,那么隨著子文件的卸載,這兩個(gè)靜態(tài)類(lèi)也會(huì)被銷(xiāo)毀了。但是我在主文件中也包含了LoaderMax類(lèi),這個(gè)定義會(huì)覆蓋掉我在子文件中的定義。于是造成的情況就是:一個(gè)主應(yīng)用程序域中的LoaderMax類(lèi)持有子應(yīng)用程序域中的ImageLoader類(lèi)的引用。這就是子文件無(wú)法卸載的原因!

    ?

    解決方法很簡(jiǎn)單:要么在主文件中也包含ImageLoader類(lèi)的定義,要么在主文件中刪去LoaderMax類(lèi)。這樣我們就解決了一個(gè)由于跨域的靜態(tài)類(lèi)引用造成的內(nèi)存泄漏問(wèn)題。

    ?

    從這個(gè)例子我們還可以總結(jié)一下在ActionScript中靜態(tài)類(lèi)、靜態(tài)變量及其衍生的單例的注意事項(xiàng),這也是和其他編程語(yǔ)言不同的地方:

    ?

    • 只要靜態(tài)類(lèi)的定義是在子應(yīng)用程序域里的,那么是可以被卸載的。
    • 靜態(tài)類(lèi)、單例的只能保證在同一個(gè)應(yīng)用程序域里的唯一性。也就是說(shuō)有可能單例不單。
    • 真正保證靜態(tài)類(lèi)和單例的唯一性的方法是把它們的定義加入到主應(yīng)用程序域。

    ?

    這種靜態(tài)類(lèi)之間引用的問(wèn)題也是唯一讓Profiler束手無(wú)策的情況,如果以后能在Profiler中直接看到類(lèi)定義來(lái)自哪個(gè)應(yīng)用程序域就更好了。

    ?

    除此之外還要小心的是靜態(tài)類(lèi)的方法可能造成的對(duì)象引用問(wèn)題,比如:Flash組件的FocusManager.setFocus(),以及Flex框架中的StyleManager的樣式注冊(cè)等等。這篇文章詳細(xì)討論了Flex模塊的卸載問(wèn)題。

    ?

    病例三:延時(shí)刪除

    ?

    這個(gè)無(wú)法卸載的問(wèn)題來(lái)自于我的一個(gè)使用Robotlegs和模塊插件開(kāi)發(fā)的子文件。為了讓所有mediator執(zhí)行自己的onRemove()方法,我在ShutdownCommand中將所有視圖從contextView上移除,此外還進(jìn)行了model和service自己的清理工作。這通常運(yùn)行良好,能夠正確的將模塊卸載。但是我卻遇到了一個(gè)問(wèn)題,嚴(yán)格來(lái)說(shuō),這并不是一個(gè)GC的問(wèn)題。因?yàn)槲彝ㄟ^(guò)trace發(fā)現(xiàn)mediator的onRemove()方法并沒(méi)有執(zhí)行!

    ?

    沒(méi)有執(zhí)行清理當(dāng)然就有可能造成內(nèi)存泄漏,那么到底是什么原因,讓我從contextView上移除視圖的時(shí)候沒(méi)有觸發(fā)對(duì)應(yīng)mediator的onRemove()方法呢?

    ?

    答案是Robotlegs的延時(shí)機(jī)制。為了兼容Flex框架,mediator的onRemove方法并不是在視圖的REMOVED_FROM_STAGE事件監(jiān)聽(tīng)里執(zhí)行的,而是延遲了一幀(查看代碼)。這樣在真正的移除代碼執(zhí)行以前我的視圖就已經(jīng)從stage上移除了,也就過(guò)不了330行那個(gè)檢查。

    ?

    于是我就只好遷就一下Robotlegs,把子文件從顯示列表上移除的時(shí)間也延遲了一幀,這樣問(wèn)題就解決了。

    ?

    從這幾個(gè)例子我們可以看出,內(nèi)存泄漏的病因可能千奇百怪,但歸根結(jié)底肯定都是某種引用沒(méi)有被釋放的問(wèn)題。在實(shí)際項(xiàng)目中,建議大家一邊開(kāi)發(fā)一邊就要測(cè)試內(nèi)存泄漏。不要到了項(xiàng)目的最后階段再來(lái)排查,那樣復(fù)雜度太高。此外,在引入第三方類(lèi)庫(kù)的時(shí)候,也要特別注意是否會(huì)引起內(nèi)存泄漏。

    ?

    本文總結(jié)了排查內(nèi)存泄漏的方法,分析了若干可能引起內(nèi)存泄漏的代碼問(wèn)題。希望對(duì)大家有所幫助。如果同學(xué)們?cè)谧约旱捻?xiàng)目中也遇到過(guò)一些疑難雜癥,歡迎留言一起探討。

    ?

    轉(zhuǎn)載于:https://www.cnblogs.com/tinytiny/archive/2012/12/19/2824665.html

    總結(jié)

    以上是生活随笔為你收集整理的演义群侠传(七)【GC垃圾回收】的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。

    如果覺(jué)得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。