本文主要記錄Mono源碼中會(huì)因?yàn)镚C的問題,造成Unity游戲不可避免的都會(huì)存在一定得內(nèi)存泄露問題的底層原因耕驰,涉及到Mono源碼中GC機(jī)制的邏輯据德。
前提條件:
要出現(xiàn)這種內(nèi)存泄露肩民,必須先準(zhǔn)備一塊任意的內(nèi)存塊:(無任何外部引用般此,理論上應(yīng)該會(huì)在用完后被GC蚪战,但在該BUG下會(huì)錯(cuò)誤的泄露,不被GC掉)
byte[] buffer = new byte[2097168]; // 內(nèi)存起始地址:0xbe82f000铐懊,內(nèi)存結(jié)束地址:0xde468c50 內(nèi)存占用大醒!:2 MB
NOTE:大小任意,越大越容易被泄露科乎。
一塊struct結(jié)構(gòu)的數(shù)組:(struct內(nèi)必須有一個(gè)類似指針的值類型, 如int壁畸,和另一個(gè)引用類型,如string)
struct Slot {
int hashCode;
String value;
}
Slot[] slots = new Slot[5000]; // 內(nèi)存起始地址:0xde45f000茅茂, 內(nèi)存結(jié)束地址:0xde468c50捏萍, 內(nèi)存占用大小:40016 B,
NOTE:大小任意空闲,數(shù)組內(nèi)的元素越多越容易觸發(fā)泄露令杈。例如 HashSet<String> 內(nèi)部使用了該數(shù)據(jù)格式。
通過在GC中打點(diǎn)碴倾,和使用GDB調(diào)用GC過程这揣,以便觀察所有對(duì)象的分配和GC的過程發(fā)現(xiàn):buffer對(duì)象錯(cuò)誤的被slots對(duì)象引用,導(dǎo)致buffer對(duì)象無法被正常GC影斑,造成內(nèi)存泄露。
原因分析:
首先對(duì)于mono/il2cpp的Boehm GC庫而言机打, mono/il2cpp的對(duì)象在分配內(nèi)存的時(shí)候矫户,會(huì)有幾種類型:
- NORMAL:無類型的內(nèi)存分配,對(duì)于GC而言残邀,因?yàn)闊o法得到對(duì)象的類型元數(shù)據(jù)皆辽,所以在做GC時(shí)會(huì)按指針對(duì)齊的方式掃描該內(nèi)存塊柑蛇,只要發(fā)現(xiàn)類似通過指針校驗(yàn)的地址都會(huì)認(rèn)為該對(duì)象引用了該指針地址指向的對(duì)象。
- PTRFREE:無指針內(nèi)存分配驱闷,明確告知GC耻台,該對(duì)象內(nèi)無任何指針信息,即在GC時(shí)無需查找該對(duì)象內(nèi)是否引用其他對(duì)象空另。在mono/il2cpp中的int型的數(shù)組盆耽,byte型的數(shù)組,字符串等使用該類型的內(nèi)存分配扼菠。
- TYPED: 有類型的內(nèi)存分配摄杂,讓GC知曉該對(duì)象的內(nèi)存布局,在做GC時(shí)無需盲目的掃描內(nèi)存查找引用的指針循榆,而直接按照對(duì)象的內(nèi)存布局查找指針地址析恢,建立對(duì)象和對(duì)象之間的引用關(guān)系,在mono/il2cpp中的class類型的對(duì)象是使用該類型的內(nèi)存分配秧饮。
而在本例中:slots的分配是用NORMAL類型映挂,buffer對(duì)象的分配是用PTRFREE類型。
因此在做GC的時(shí)候盗尸,對(duì)于slots對(duì)象柑船,GC會(huì)掃描該對(duì)象的內(nèi)存區(qū)間,查找其內(nèi)部的指針地址振劳,即從0xde45f000到0xde468c50地址按照指針對(duì)齊的方式查找指針地址:
例如:0xde45f000 0xde464d44 0xde464f40 0xde46513c ....
其中出現(xiàn)了 0xde464f40 這個(gè)地址的值剛好為:0xbe82f000(即Slot結(jié)構(gòu)體內(nèi)hashCode的值)椎组,而GC會(huì)錯(cuò)誤的將該int型數(shù)值當(dāng)做指針,而該指針剛好又指向了一塊GC托管的內(nèi)存塊历恐,即buffer對(duì)象寸癌,因此GC認(rèn)為該buffer對(duì)象被slots對(duì)象內(nèi)部引用了,buffer對(duì)象也被GC標(biāo)記弱贼,不會(huì)被釋放蒸苇。
該問題的關(guān)鍵在于,GC將slot結(jié)構(gòu)體內(nèi)的hashcode這個(gè)int值錯(cuò)誤的當(dāng)做的指針吮旅,而該int值剛好又指向了另一個(gè)托管的對(duì)象溪烤,因此GC錯(cuò)認(rèn)為了兩個(gè)對(duì)象存在引用關(guān)系,而造成內(nèi)存泄露庇勃。
最小化Demo:
public class NewBehaviourScript : MonoBehaviour {
struct Slot
{
public int hashCode;
public string value;
}
private static Slot[] slots = new Slot[5000];
void Start () {
for(var i = 0; i < slots.Length; ++i) {
slots[i].hashCode = i * (1024 * 1023) + 1;
}
}
void Update () {
byte[] buffer = new byte[2 * 1024 * 1024]; // memory leak
}
}
將struct Slot修改為class Slot檬嘀,則可修復(fù)內(nèi)存泄露問題,因?yàn)閏lass對(duì)象的內(nèi)存分配時(shí)TYPED類型责嚷。
因?yàn)镸ono的GC的設(shè)計(jì)問題鸳兽,Unity游戲中幾乎不可避免的都會(huì)隨著時(shí)間出現(xiàn)內(nèi)存泄露問題,因?yàn)槔鏗ashSet這種數(shù)據(jù)結(jié)構(gòu)內(nèi)部都會(huì)出現(xiàn)該問題罕拂。但我們可以做的事情揍异,依然是內(nèi)存使用的兩大真理(特別是虛擬機(jī)類型的語言):
- 減少頻繁分配小內(nèi)存
- 減少一次性分配大內(nèi)存
這樣做全陨,不能完全避免Mono的底層GC問題,但是它可以讓這種內(nèi)存泄露的變得更加平緩衷掷。
NOTE ATTRIBUTES
Created Date: 2019-11-12 10:28:06
Last Evernote Update Date: 2020-05-23 07:43:32