什么是Mono內(nèi)存
對于目前絕大多數(shù)基于Unity引擎開發(fā)的項目而言,其托管堆內(nèi)存是由Mono分配和管理的《湎模“托管” 的本意是Mono可以自動地改變堆的大小來適應你所需要的內(nèi)存良价,并且適時地調(diào)用垃圾回收(Garbage Collection)操作來釋放已經(jīng)不需要的內(nèi)存,從而降低開發(fā)人員在代碼內(nèi)存管理方面的門檻搏色。Unity游戲在運行時的內(nèi)存占用情況可以用下圖表示:
目前絕大部分Unity游戲邏輯代碼所使用的語言為C#善茎,C#代碼所占用的內(nèi)存又稱為mono內(nèi)存,這是因為Unity是通過mono來跨平臺解析并運行C#代碼的频轿,在Android系統(tǒng)上垂涯,游戲的lib目錄下存在的libmono.so文件,就是mono在Android系統(tǒng)上的實現(xiàn)航邢。C#代碼通過mono解析執(zhí)行耕赘,所需要的內(nèi)存自然也是由mono來進行分配管理,下面就介紹一下mono的內(nèi)存管理策略以及內(nèi)存泄漏分析膳殷。
Mono內(nèi)存管理策略
Mono通過垃圾回收機制(Garbage Collect操骡,簡稱GC)對內(nèi)存進行管理。Mono內(nèi)存分為兩部分赚窃,已用內(nèi)存(used)和堆內(nèi)存(heap)册招,已用內(nèi)存指的是mono實際需要使用的內(nèi)存,堆內(nèi)存指的是mono向操作系統(tǒng)申請的內(nèi)存勒极,兩者的差值就是mono的空閑內(nèi)存是掰。當mono需要分配內(nèi)存時,會先查看空閑內(nèi)存是否足夠辱匿,如果足夠的話冀惭,直接在空閑內(nèi)存中分配,否則mono會進行一次GC以釋放更多的空閑內(nèi)存掀鹅,如果GC之后仍然沒有足夠的空閑內(nèi)存散休,則mono會向操作系統(tǒng)申請內(nèi)存,并擴充堆內(nèi)存乐尊,具體如下圖所示戚丸。
通過上文可知,GC的主要作用在于從已用內(nèi)存中找出那些不再需要使用的內(nèi)存扔嵌,并進行釋放限府。Mono中的GC主要有以下幾個步驟:
1.停止所有需要mono內(nèi)存分配的線程。
2.遍歷所有已用內(nèi)存痢缎,找到那些不再需要使用的內(nèi)存胁勺,并進行標記。
3.釋放被標記的內(nèi)存到空閑內(nèi)存独旷。
4.重新開始被停止的線程署穗。
除了空閑內(nèi)存不足時mono會自動調(diào)用GC外寥裂,也可以在代碼中調(diào)用GC.Collect()手動進行GC,但是案疲,GC本身是比較耗時的操作封恰,而且由于GC會暫停那些需要mono內(nèi)存分配的線程(C#代碼創(chuàng)建的線程和主線程),因此無論是否在主線程中調(diào)用褐啡,GC都會導致游戲一定程度的卡頓诺舔,需要謹慎處理。另外备畦,GC釋放的內(nèi)存只會留給mono使用低飒,并不會交還給操作系統(tǒng),因此mono堆內(nèi)存是只增不減的懂盐。
Mono內(nèi)存泄漏分析
Mono是如何判斷已用內(nèi)存中哪些是不再需要使用的呢逸嘀?是通過引用關系的方式來進行的。Mono會跟蹤每次內(nèi)存分配的動作允粤,并維護一個分配對象表崭倘,當GC的時候,以全局數(shù)據(jù)區(qū)和當前寄存器中的對象為根節(jié)點类垫,按照引用關系進行遍歷司光,對于遍歷到的每一個對象,將其標記為活的(alive)悉患。
如上圖所示残家,假設A是處于全局數(shù)據(jù)區(qū)的一個對象,那么在GC的時候?qū)⒆鳛楦?jié)點進行遍歷售躁,由于B坞淮、C、D對象都可以由A遍歷到陪捷,因此被標記為活的回窘,E、F對象則沒有被標記市袖。注意啡直,由于引用關系是單向的,A引用了B并不代表B也引用了A苍碟,所以遍歷也只能單向進行酒觅。
由于GC以全局數(shù)據(jù)區(qū)和當前寄存器中的對象為根節(jié)點進行遍歷,所以對象的被標記意味著該對象可以通過全局對象或者當前上下文訪問到微峰,而沒有被標記的對象則意味著該對象無法通過任何途徑訪問到舷丹,即該對象“失聯(lián)”了,GC最終會將所有“失聯(lián)”的對象內(nèi)存進行回收蜓肆,上圖中的E和F將會在GC過程中被回收颜凯。
既然mono已經(jīng)有了完善的GC機制谋币,那是否還會存在內(nèi)存泄漏呢?答案是肯定的装获,只是此處的內(nèi)存泄漏需要重新定義一下瑞信,我們把對象已經(jīng)不再需要使用卻沒有被GC回收的情況稱為mono內(nèi)存泄漏厉颤。Mono內(nèi)存泄漏會使空閑內(nèi)存減少穴豫,GC頻繁,mono堆不斷擴充逼友,最終導致游戲內(nèi)存占用的升高精肃。下圖就是一個mono內(nèi)存泄漏的例子。
解決辦法
對于mono內(nèi)存泄漏帜乞,一般只能通過猜測+不斷修改代碼測試的方法來修復問題司抱,效率很低,騰訊Wetest平臺的Cube工具提供了mono內(nèi)存快照對比的功能黎烈,并包括對象分配堆棧习柠,對象引用關系等詳細信息,是定位mono內(nèi)存泄漏問題的一大利器照棋。下面結(jié)合具體的代碼嘗試使用Cube定位mono內(nèi)存泄漏問題资溃。首先我們定義類A,并在A的構(gòu)造函數(shù)中申請了一塊int[1000]大小的內(nèi)存烈炭。
接著我們定義A類型的靜態(tài)變量objectA溶锭,在游戲界面上繪制一個按鈕,并在按鈕點擊事件中給objectA賦值符隙,此時新生成了new int[1000]對象趴捅,并由objectA引用。
使用Cube的mono內(nèi)存檢測功能霹疫,并在按鈕按下之前和按下之后分別進行一次快照拱绑,對比兩次快照,查看快照間新增對象丽蝎。
可以看到欺栗,按鈕按下前后新增的最大對象即為代碼中生成的new int[1000]對象,并且該對象被引用的次數(shù)為1征峦,為了查看詳細的引用關系迟几,下載快照文件snapshot2,其中有這樣兩行數(shù)據(jù):
第一行說明在OnGUI函數(shù)中生成了一個A類型的對象栏笆,其指針為1533098928类腮,第二行說明在OnGUI()->A:.cotr()中生成了一個Int32[]類型的對象,并且該對象被指針為1533098928的對象引用蛉加。即new int[1000]對象被objectA引用蚜枢,這也是導致new int[1000]對象無法被GC回收的原因缸逃。而objectA本身是一個靜態(tài)對象,是GC的根節(jié)點厂抽,因此沒有對象引用需频。
如果需要生成的new int[1000]對象被回收怎么做呢?很簡單筷凤,將objectA.a設置為null昭殉,沒有了objectA對其的引用,自然會被GC回收了藐守。需要說明的是挪丢,將objectA.a設置為null只是斷絕了引用關系,真正對象的回收要等到GC的時候才會進行卢厂,Cube在獲取內(nèi)存快照的時候會首先進行一次GC乾蓬,防止由于沒有及時調(diào)用GC導致的誤判。
游戲中大部分mono內(nèi)存泄漏的情況都是由于靜態(tài)對象的引用引起的慎恒,因此對于靜態(tài)對象的使用需要特別注意任内,盡量少用靜態(tài)對象,對于不再需要的對象將其引用設置為null融柬,使其可以被GC及時回收死嗦,但是由于游戲代碼過于復雜,對象間的引用關系層層嵌套丹鸿,真正操作起來難度很大越走。可以首先使用Cube工具進行分析靠欢,根據(jù)mono內(nèi)存趨勢找出泄漏的具體場景廊敌,然后再使用快照對比功能進行詳細分析。