GC介紹
- GC(垃圾回收)是指不再被用到(廢棄、非激活狀態(tài))數(shù)據(jù)的內(nèi)存回收再次使用的過(guò)程(主要針對(duì)的是堆內(nèi)存的內(nèi)存管理)锨匆。
Unity內(nèi)存管理機(jī)制介紹
- Unity的內(nèi)存管理機(jī)制是采用自動(dòng)內(nèi)存管理的形式(減少開(kāi)發(fā)者對(duì)于內(nèi)存管理的關(guān)注,提高開(kāi)發(fā)效率)
- Unity的內(nèi)存管理區(qū)分:(1)堆棧內(nèi)存stack(用于存儲(chǔ)短時(shí)數(shù)據(jù)和較小數(shù)據(jù))(2)堆內(nèi)存heap(用于存儲(chǔ)長(zhǎng)時(shí)數(shù)據(jù)和較大數(shù)據(jù))
- 變量一旦被激活港华,其占用的內(nèi)存塊的狀態(tài)標(biāo)志會(huì)標(biāo)記為使用狀態(tài)脾歇,而變量不再被激活,其占用的內(nèi)存塊亦會(huì)被標(biāo)記為空閑狀態(tài)逗旁,則會(huì)被Unity的GC機(jī)制所檢測(cè)回收(堆棧上的回收是即時(shí)以及快速的夯秃,而堆內(nèi)存上并不是即時(shí)的,GC是一個(gè)定時(shí)的檢測(cè)回收機(jī)制痢艺,只有GC時(shí)才會(huì)回收堆內(nèi)存空間)仓洼。
堆棧上的內(nèi)存分配與回收機(jī)制介紹(值類(lèi)型變量)
類(lèi)似于stack(棧),創(chuàng)建即進(jìn)堤舒,無(wú)效則出色建,以一種順序且可控的形式進(jìn)行。(操作十分簡(jiǎn)潔舌缤,且快捷方便)
堆上的內(nèi)存分配與回收機(jī)制介紹(非值類(lèi)型變量)
相較于堆棧更復(fù)雜箕戳,且其回收順序是不可控的。
變量存儲(chǔ)在堆上的大致步驟:
- Unity會(huì)首先檢測(cè)堆內(nèi)存国撵,確定是否有足夠的閑置內(nèi)存單元來(lái)存儲(chǔ)變量陵吸,如果有,直接分配其對(duì)應(yīng)大小的內(nèi)存單元介牙。沒(méi)有則跳轉(zhuǎn)至第2步壮虫。
- 因?yàn)闆](méi)有足夠的存儲(chǔ)單元,此時(shí)Unity就會(huì)觸發(fā)GC(垃圾回收)機(jī)制來(lái)釋放那些被標(biāo)志為不再需要的堆內(nèi)存塊(該步驟十分緩慢)环础。如果此時(shí)垃圾回收后已經(jīng)有了足夠能存儲(chǔ)變量的內(nèi)存單元囚似,則直接進(jìn)行內(nèi)存分配,否則线得,跳轉(zhuǎn)至第3步饶唤。
- 因?yàn)槔厥蘸筮€是沒(méi)有足夠的存儲(chǔ)單元,此時(shí)Unity則會(huì)直接擴(kuò)展堆內(nèi)存的大小(該步驟極其緩慢)贯钩,然后再分配對(duì)應(yīng)大小的內(nèi)存單元給變量募狂。
GC的大致操作
- 遍歷檢測(cè)堆內(nèi)存上的變量。
- 檢測(cè)每個(gè)變量的引用的激活狀態(tài)角雷。
- 如果變量的引用不再處于激活態(tài)祸穷,則將其標(biāo)識(shí)為可回收狀態(tài)。
- 移除被標(biāo)識(shí)為可回收狀態(tài)的變量谓罗,將其所占有的內(nèi)存都回收到堆內(nèi)存上粱哼。
GC的觸發(fā)機(jī)制
- 在堆內(nèi)存上進(jìn)行內(nèi)存分配而內(nèi)存不夠的時(shí)候,觸發(fā)檩咱。
- 自動(dòng)觸發(fā)揭措。(不同平臺(tái)胯舷,運(yùn)行頻率不一樣)
- 強(qiáng)制觸發(fā)。(開(kāi)發(fā)者調(diào)用相應(yīng)函數(shù))
GC會(huì)帶來(lái)的問(wèn)題
- 降低游戲幀率绊含,使其運(yùn)行緩慢桑嘶。
- 因?yàn)镚C操作可能會(huì)需要大量的時(shí)間來(lái)運(yùn)行,尤其是在其操作不僅進(jìn)行單純的內(nèi)存分配躬充,而且后續(xù)還進(jìn)行了內(nèi)存回收和內(nèi)存擴(kuò)充操作的時(shí)候逃顶。這種影響會(huì)在游戲運(yùn)行到關(guān)鍵時(shí)刻被放大,造成嚴(yán)重后果
- 如果存在大量的變量和引用充甚,那么僅僅在第一步內(nèi)存檢測(cè)時(shí)以政,就會(huì)花費(fèi)大量的運(yùn)行時(shí)間。
- 堆內(nèi)存的碎片化伴找。
- 內(nèi)存在分配時(shí)是會(huì)有不同的大小的盈蛮,當(dāng)它們被回收時(shí),其大小是不會(huì)改變的技矮,這就使得其在回收后依舊是其本身在分配后切割的大小抖誉,這就造成了一個(gè)結(jié)果——總的可使用的內(nèi)存單元較大,但單獨(dú)的內(nèi)存單元較小衰倦。內(nèi)存分配時(shí)袒炉,如果匹配不到合適大小的存儲(chǔ)單元,則依舊會(huì)觸發(fā)GC操作樊零,甚至是堆內(nèi)存擴(kuò)展操作我磁。
- 碎片化造成的實(shí)際效果:
(1). 游戲占用內(nèi)存越來(lái)越大。(經(jīng)過(guò)了堆擴(kuò)展)
(2). GC會(huì)更加頻繁地被觸發(fā)淹接。
降低GC影響的方法
大致從三個(gè)方面入手:
- 減少GC的運(yùn)行次數(shù)
- 減少單次GC的運(yùn)行時(shí)間
- 控制GC的觸發(fā)時(shí)刻十性,避免關(guān)鍵時(shí)刻觸發(fā)
降低影響的策略:
- 重構(gòu)代碼:
- 減少堆內(nèi)存分配(變量和引用的創(chuàng)建)叛溢,這樣就減少了GC操作中變量的檢測(cè)數(shù)量塑悼,從而提高GC運(yùn)行效率。
- 降低堆內(nèi)存分配和回收的頻率:
- 根據(jù)測(cè)試方案楷掉,調(diào)整GC的觸發(fā)時(shí)刻厢蒜,使其按照可預(yù)測(cè)的順序執(zhí)行。
實(shí)用方法:
- 緩存:對(duì)沒(méi)有改變卻被反復(fù)調(diào)用分配的變量進(jìn)行保存烹植,重復(fù)使用斑鸦。
- 不在頻繁調(diào)用的函數(shù)中進(jìn)行堆內(nèi)存分配。
- 容器類(lèi)草雕,不進(jìn)行多次創(chuàng)建巷屿,而是對(duì)單獨(dú)容器把持后,使用前進(jìn)行清理即可重復(fù)利用墩虹。
- 對(duì)象池:對(duì)需要頻繁的創(chuàng)建和銷(xiāo)毀的對(duì)象嘱巾,將其保存在固定容器中憨琳,反復(fù)回收利用,僅需要設(shè)置狀態(tài)即可旬昭。
造成不必要的堆內(nèi)存分配的因素
字符串:
C#中篙螟,字符串是引用類(lèi)型變量,而不是值類(lèi)型變量问拘,且其不可改變遍略,每次對(duì)其進(jìn)行變值操作,實(shí)質(zhì)都是會(huì)新建一個(gè)字符串來(lái)存儲(chǔ)骤坐,而舊的字符串會(huì)產(chǎn)生內(nèi)存垃圾绪杏,被廢棄回收。
因此可以采取一些方法來(lái)弱化字符串所帶來(lái)的影響:
- 減少不必要的字符串創(chuàng)建纽绍。(緩存多次利用的字符串)
- 減少不必要的字符串操作寞忿,可以分離不變值字符串,以及常變值字符串顶岸。
- 使用StringBuilder類(lèi)替代經(jīng)常變動(dòng)的字符串創(chuàng)建腔彰。
- 移除Debug.log()函數(shù),該函數(shù)即使輸出為空也會(huì)創(chuàng)建至少一個(gè)字符的字符串辖佣,如果游戲中大量調(diào)用霹抛,則會(huì)造成內(nèi)存垃圾的增加。
Unity函數(shù)調(diào)用:
在調(diào)用Unity中自帶的函數(shù)方法時(shí)卷谈,很容易在頻繁調(diào)用時(shí)杯拐,產(chǎn)生大量的內(nèi)存垃圾,需要根據(jù)實(shí)際來(lái)檢測(cè)使用世蔗。
例如:調(diào)用GameObject.name或者GameObject.tag時(shí)也會(huì)造成預(yù)想不到的堆內(nèi)存分配端逼,這兩個(gè)函數(shù)都會(huì)將結(jié)果存為新的字符串返回,這就造成了不必要的內(nèi)存垃圾污淋。
而為了避免這種情況顶滩,我們可以換用Unity中的GameObject.CompareTag函數(shù)來(lái)替代。
裝箱操作:
裝箱:當(dāng)值類(lèi)型作為引用類(lèi)型來(lái)使用時(shí)寸爆,觸發(fā)裝箱操作礁鲁,C#會(huì)將值類(lèi)型通過(guò)System.Object類(lèi)引用來(lái)封裝。
應(yīng)該盡量避免裝箱操作赁豆。
攜程:
調(diào)用StartCoroutine函數(shù)會(huì)產(chǎn)生少量的內(nèi)存垃圾仅醇,因?yàn)閁nity會(huì)生成實(shí)體來(lái)管理攜程,所以應(yīng)該在游戲的關(guān)鍵時(shí)刻嚴(yán)格限制攜程的調(diào)用魔种。
yield在攜程中不會(huì)產(chǎn)生堆內(nèi)存分配析二,但是如果它帶有參數(shù)返回,則會(huì)造成不必要的內(nèi)存垃圾节预。
yield return 0;
由于需要返回0叶摄,這里其實(shí)引發(fā)了裝箱操作漆改,所以產(chǎn)生了內(nèi)存垃圾,避免措施可以這樣做:
yeild return null;
另一種對(duì)攜程的錯(cuò)誤使用是准谚,在每次返回的時(shí)候都new同一個(gè)變量挫剑。
while(!isComplete)
{
yield return new WaitForSeconds(1f);
}
這里可以采用緩存來(lái)避免內(nèi)存垃圾的產(chǎn)生:
WaitForSeconds delay = new WaitForSeconds(1f);
while(!isComplete)
{
yield return delay;
}
foreach循環(huán)
在Unity5.5以前的版本,foreach的迭代中都會(huì)生成內(nèi)存垃圾柱衔,因?yàn)樵诘鷷r(shí)樊破,都會(huì)在堆內(nèi)存上生產(chǎn)一個(gè)System.Object用來(lái)實(shí)現(xiàn)迭代循環(huán)操作,在5.5版本后解決了這個(gè)版本唆铐。
所以在5.5版本前哲戚,可以采用for或者while循環(huán)來(lái)替代方案。
函數(shù)的引用
函數(shù)的引用艾岂,無(wú)論其指向的函數(shù)類(lèi)型顺少,都會(huì)在堆內(nèi)存上進(jìn)行內(nèi)存分配,所以最好減少函數(shù)的引用王浴。
LINQ和常量表達(dá)式
由于LINQ和常量表達(dá)式都是以裝箱的形式實(shí)現(xiàn)脆炎,所以使用時(shí)最好進(jìn)行性能測(cè)試。
重構(gòu)代碼來(lái)減小GC的影響
即使我們減小了代碼在堆內(nèi)存上的分配操作氓辣,代碼也會(huì)增加GC的工作量秒裕。最常見(jiàn)的增加GC工作量的方式是讓其檢查它不必檢查的對(duì)象。struct是值類(lèi)型的變量钞啸,但是如果struct中包含有引用類(lèi)型的變量几蜻,那么GC就必須檢測(cè)整個(gè)struct。如果這樣的操作很多体斩,那么GC的工作量就大大增加梭稚。在下面的例子中struct包含一個(gè)string,那么整個(gè)struct都必須在GC中被檢查:
public struct ItemData
{
public string name;
public int cost;
public Vector3 position;
}
private ItemData[] itemData;
我們可以將該struct拆分為多個(gè)數(shù)組的形式絮吵,從而減小GC的工作量:
private string[] itemNames;
private int[] itemCosts;
private Vector3[] itemPositions;
簡(jiǎn)而言之弧烤,就是要減少GC檢測(cè)不必要變量的次數(shù)。
定時(shí)執(zhí)行GC操作源武。
在場(chǎng)景切換或是其他并不影響游戲性能的情況下扼褪,可以主動(dòng)進(jìn)行GC操作:
System.GC.Collec()