演义群侠传(七)【GC垃圾回收】
在《給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引用了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)用弱引用的方式:
由于弱引用不計(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ì)象中:
在清除foo的引用之前執(zhí)行destroy()方法:
經(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à):
另一個(gè)示例的destroy()方法演示了對(duì)數(shù)組中對(duì)象的處理方法:
在結(jié)構(gòu)更復(fù)雜的項(xiàng)目里,我們還可以抽象出一個(gè)IDestroyable接口,讓需要執(zhí)行清理的自定義對(duì)象實(shí)現(xiàn)這個(gè)接口。這樣我們的清理代碼可以寫(xiě)為:
總結(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)題。
- 上一篇: QQ首页的问题
- 下一篇: 简易zlib库解压缩函数封装