演义群侠传(七)【GC垃圾回收】
在《給AS程序員的一點建議一文》中我提到了釋放資源的重要性。最近在一些項目過程中我又對這方面有了更多的理解,在此希望能夠分享給大家。首先讓我們來回顧一下關于垃圾回收(Garbage Collection,下文簡稱GC)的一些知識。要閱讀本文,你需要對GC機制有些基本認識。
在ActionScript中,我們沒有API可以直接刪除一個對象,也不能控制Player進行GC。但是GC的行為是可以預估的,作為開發者,我們需要了解的是GC執行的時機是發生在需要向操作系統請求分配內存的時候。
從上面的模擬圖我們可以看到:
- Player以塊的方式請求和釋放內存。GC的結果不一定就是更少的內存占用,也有可能是從操作系統獲得更多的可用內存。
- Player會在某些GC過程中把內存中未使用部分組合成可以釋放的塊還給操作系統。
- 此外還要注意的是Player為了避免占用太多的CPU資源,會將一些GC操作分到不同的時間片中運行,所以一次GC過程并不一定清理完所有可回收資源。
一次GC過程(GC Pass)分為以下兩個步驟:
Reference Counting
統計所有對象的引用計數,如果某個對象沒有任何引用,就標記為可回收。
這個操作很好理解,需要強調的是weak reference(弱引用)是不參與計算的。引用計數是一個相對省CPU的操作,能夠篩選出大部分可回收資源,但是對一些循環引用的情況就無能為力了。在下圖中,標記為綠色的對象每個的引用數都為1,但它們明顯是應該被回收的。
所以GC需要進行第二個步驟:
Mark Sweeping
這個步驟是從根對象(Root)開始輪詢對象的引用。所謂的根對象包括:
- Stage對象
- 靜態變量
- 局部變量
這種方式足夠精確,能夠成功篩選出上圖中綠色標記的對象,而它的代價就是較大的計算開銷。
為了幫助GC過程更高效的執行,最好是能在第一步引用計數中就把需要回收的對象都標記出來。具體的做法就是把所有不需要的對象引用全部清空,包括:
- 刪除成員變量的引用
- 從可視對象列表上移除對象
- 移除事件監聽
難點:事件監聽是否會造成對象不能回收?這個問題要具體分析,有些情況可以,有些情況卻不可以。歸根結底還是引用關系的問題。來看下面這個例子:
我們看到foo引用了bar,而bar又通過事件監聽的聯系引用了foo,這就構成了一個循環引用。根據前文對GC步驟的分析,這兩個對象都必須到第二步mark and sweep才能被標記出來。
如果我們對事件監聽用弱引用的方式:
由于弱引用不計入引用計數,所以現在foo的引用數為0。GC在第一步操作中就能把foo標記出來,從而減少了一些運算開銷。這也是為什么Grant Skinner呼吁把弱引用作為事件監聽的默認方式的原因。
但是作為最佳實踐,我們還是提倡要手動移除事件監聽。以下代碼添加一個destroy()方法(也有習慣命名為dispose()或kill()等)到Foo對象中:
在清除foo的引用之前執行destroy()方法:
經過我們如此處理,兩個對象的引用數全都為0。GC只需第一步處理就完成任務,我們節約了更多的運算開銷!
從上例可以看出在一般情況下,監聽子對象的事件不會影響GC。不管是弱引用方式的監聽,還是顯式移除監聽,都只是幫助GC更高效運行的手段,而不會影響GC的結果。但是如果事件監聽造成的是對象以外的引用關系,情況就不同了,并且很有可能造成回收失敗。一個常見的錯誤例子是監聽Stage對象的RESIZE事件,如果沒有顯式移除這個監聽或者是沒有采用弱引用方式,那么這個對象就不會被GC回收的。所以我建議大家還是要盡可能的顯式移除監聽,切斷引用關系。
我們現在已經用對GC最友好的方式做好了清理準備,但是對象還沒有從內存中刪除。在等待GC執行的這段時間,對象內的代碼還在繼續執行。比如加在對象上的ENTER_FRAME事件監聽處理還在繼續執行,對象內的MovieClip或Sound都還在繼續播放。我們一定要避免這種情況的發生,所以在切斷引用之前,我們還要在destroy()方法中做些清理工作。我們要做的工作包括:
- clearInterval(),clearTimeout()
- timer.stop()
- loader.unload()/loader.unloadAndStop()
- movieclip.stop() 如果有子MC的,也要停止播放
- bitmapData.dispose()
- 關閉LocalConnection,NetConnection,NetStream
- 停止音頻和視頻的播放
- 刪除Camera和Microphone對象的引用
- 調用子對象的destroy()方法,如果有的話
其實這些都是在開發中管理資源的基本常識,歸結為一句話就是:誰創建了對象,誰就要負責清理該對象。
下面就以一些我在實際項目中開發的destroy()方法為例,看代碼說話:
另一個示例的destroy()方法演示了對數組中對象的處理方法:
在結構更復雜的項目里,我們還可以抽象出一個IDestroyable接口,讓需要執行清理的自定義對象實現這個接口。這樣我們的清理代碼可以寫為:
總結:GC好比是ActionScript城市的環衛工人,我們的每個類都是從事勞動生產的市民。優秀的市民會把生產垃圾分類安放到回收點,而不文明的市民則把垃圾丟得到處都是。你說那種做法讓城市的清掃工作變得更加高效?所以請大家謹記“誰創建誰清理”的原則,做一位ActionScript好市民。
本文回顧了GC的一些基本知識和最佳實踐。在下一篇中我將結合一些具體問題,為大家把脈GC的疑難雜癥。敬請期待。
繼續閱讀:
?
前文我們介紹了GC的工作機制和幫助GC更好工作的最佳實踐。其實只要我們遵守誰創建誰清理的原則來管理對象,就能基本上避免回收失敗,也就是我們通常說的內存泄漏問題。但是在實際項目中我們還會看到各種原因引起的內存泄漏,接下來就讓我們一起來找出病因。
?
首先我們需要觀察癥狀,也就是內存的使用曲線。排查的方法是反復執行一些創建和刪除對象的方法、反復加載和卸載子文件。如果內存曲線一路飆升、或者是居高不下,都表明發生了內存泄漏問題。觀察內存占用可以直接求助于操作系統的資源管理器,也可以用Hi-ReS-Stats這個類。
?
?
第二個需要觀察的地方,是Player輸出的load和unload信息。加載和卸載外部文件,是內存泄漏問題的重災區。在調試階段,我一般會在主文件加一個執行System.gc()語句的按鈕。一旦卸載了一個子文件,就手動觸發若干次GC。如果沒有輸出子文件的卸載信息,那么就說明出現泄漏了。
?
?
第三個可以幫助我們排查問題的地方是Profiler工具,當你刪除了對象引用,并手動觸發GC以后,可以觀察這個對象是否還存在內存中。Profiler可以說是排查內存泄漏問題的終極工具,唯一的問題就是會拖慢整體的運行速度,比較慢。
?
?
觀察到問題現象以后我們得順藤摸瓜,找出到底是那個對象占著內存不放,然后對癥下藥。下面我們就來分析幾個內存泄漏的疑難雜癥。
?
病例一:小心loaderContext和applicationDomain
?
ActionScript 3的Loader對象遠沒有我們想象中那么簡單,內存泄漏問題有很大一部分是由于不當的加載和卸載操作引起的。我在研究Gaia框架的內存泄漏問題的時候發現了一處由于沒有刪除LoaderContext的引用而造成的卸載失敗問題,其實就是沒有釋放應用程序域所造成的。應用程序域是一個需要被重視的對象,它對加載和卸載的影響有如下兩點:
?
- 如果子SWF文件是加載到主應用程序域里的,那么這個文件是不能卸載的(前提是子SWF文件內的類定義沒有被主應用程序域里定義所覆蓋)。
- 如果子SWF文件是加載到子應用程序域內(Loader的默認方式),那么這個文件是一定能夠被卸載的。
?
關于應用程序域的知識可以看我以前翻譯的文章。根據類定義在主應用程序域里的向下覆蓋原則,我們還可以考慮以下情況:如果再次加載相同的子SWF文件到主應用程序域,子文件里所包含的類定義將全部忽略,并不會注冊到主應用程序域中。這次加載的SWF文件則是可以被卸載的。換句話說,一旦類定義被加入到主應用程序域里就不能夠被刪除。而沒有加載到主應用程序域內的對象如果不能卸載就肯定是內存泄漏。
?
實際開發中除了一些確實不需要卸載的模塊代碼需要加載到主應用程序域中,一般我們還是將對象加載到子應用程序域中去的。
?
病例二:小心靜態類
?
癥狀還是某個子文件加載后不能卸載,但是當我們再次加載這個子文件的時候,能從log看到之前的子文件被釋放了。這是一個輕度內存泄漏的例子,一般不會引起內存飆升直到引起crash等強烈后果,但是我們也不能掉以輕心。
?
根據之前的經驗:不能卸載一定是某個對象被占住了,后續再次加載又能卸載之前的實例,說明前面文件中被占住的資源又被釋放了。我們先通過Profiler查看到底是那個對象被占住,然而分析下來看到居然是子文件中創建的所有實例都已經釋放了。那么,到底是什么原因呢?
?
既然實例都已經被釋放了,那么只有可能是類定義被占住了。我在這個子文件中用到了Greensock類庫的ImageLoader。通過研究它的源碼發現這個加載類庫采用了與TweenMax類似的插件機制。當我第一次引用ImageLoader定義的時候,它會自動向LoaderMax類注冊。也就是說LoaderMax類的靜態成員持有ImageLoader定義的引用。
?
如果這兩個類定義都在子應用程序域中,那么隨著子文件的卸載,這兩個靜態類也會被銷毀了。但是我在主文件中也包含了LoaderMax類,這個定義會覆蓋掉我在子文件中的定義。于是造成的情況就是:一個主應用程序域中的LoaderMax類持有子應用程序域中的ImageLoader類的引用。這就是子文件無法卸載的原因!
?
解決方法很簡單:要么在主文件中也包含ImageLoader類的定義,要么在主文件中刪去LoaderMax類。這樣我們就解決了一個由于跨域的靜態類引用造成的內存泄漏問題。
?
從這個例子我們還可以總結一下在ActionScript中靜態類、靜態變量及其衍生的單例的注意事項,這也是和其他編程語言不同的地方:
?
- 只要靜態類的定義是在子應用程序域里的,那么是可以被卸載的。
- 靜態類、單例的只能保證在同一個應用程序域里的唯一性。也就是說有可能單例不單。
- 真正保證靜態類和單例的唯一性的方法是把它們的定義加入到主應用程序域。
?
這種靜態類之間引用的問題也是唯一讓Profiler束手無策的情況,如果以后能在Profiler中直接看到類定義來自哪個應用程序域就更好了。
?
除此之外還要小心的是靜態類的方法可能造成的對象引用問題,比如:Flash組件的FocusManager.setFocus(),以及Flex框架中的StyleManager的樣式注冊等等。這篇文章詳細討論了Flex模塊的卸載問題。
?
病例三:延時刪除
?
這個無法卸載的問題來自于我的一個使用Robotlegs和模塊插件開發的子文件。為了讓所有mediator執行自己的onRemove()方法,我在ShutdownCommand中將所有視圖從contextView上移除,此外還進行了model和service自己的清理工作。這通常運行良好,能夠正確的將模塊卸載。但是我卻遇到了一個問題,嚴格來說,這并不是一個GC的問題。因為我通過trace發現mediator的onRemove()方法并沒有執行!
?
沒有執行清理當然就有可能造成內存泄漏,那么到底是什么原因,讓我從contextView上移除視圖的時候沒有觸發對應mediator的onRemove()方法呢?
?
答案是Robotlegs的延時機制。為了兼容Flex框架,mediator的onRemove方法并不是在視圖的REMOVED_FROM_STAGE事件監聽里執行的,而是延遲了一幀(查看代碼)。這樣在真正的移除代碼執行以前我的視圖就已經從stage上移除了,也就過不了330行那個檢查。
?
于是我就只好遷就一下Robotlegs,把子文件從顯示列表上移除的時間也延遲了一幀,這樣問題就解決了。
?
從這幾個例子我們可以看出,內存泄漏的病因可能千奇百怪,但歸根結底肯定都是某種引用沒有被釋放的問題。在實際項目中,建議大家一邊開發一邊就要測試內存泄漏。不要到了項目的最后階段再來排查,那樣復雜度太高。此外,在引入第三方類庫的時候,也要特別注意是否會引起內存泄漏。
?
本文總結了排查內存泄漏的方法,分析了若干可能引起內存泄漏的代碼問題。希望對大家有所幫助。如果同學們在自己的項目中也遇到過一些疑難雜癥,歡迎留言一起探討。
?
轉載于:https://www.cnblogs.com/tinytiny/archive/2012/12/19/2824665.html
總結
以上是生活随笔為你收集整理的演义群侠传(七)【GC垃圾回收】的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: QQ首页的问题
- 下一篇: 白芸豆黑咖啡学生可以喝吗,这个我们小孩能