本文主要是Unity官方優化文檔的筆記,可直接跳轉到官方文檔查看

Best practice guides

1、unity的內存池有幾種?

stack內存池和heap內存池,也就是棧內存和堆內存,棧和託管堆

2、棧內存和堆內存的區別是什麼?

棧就像數據結構中的stack,當變數存儲在棧上,內存從棧的末端分配,當變數不在作用域時,內存就立即返回到棧中繼續被重用;棧內存的分配和回收十分便捷

棧的結構是後進先出LIFO,只能使用棧頂的數據,想要讀取某個元素就要將之前的元素全部出棧,才能完成

堆的結構是無序的表,託管堆內存的分配和回收經常會牽扯託管堆上不同的內存塊

a.申請速度

棧的申請速度較快,但卻不受程序員的控制

堆的申請速度較慢,容易產生內存碎片,但是用起來比較方便

b.申請大小

棧申請的容量較小1-2M

堆申請大小雖受限於系統中有效的虛擬內存,但較大

c.存儲內容

棧主要保存線程代碼的執行位置和一些調用代碼的數據

堆主要保存對象和數據

d.管理者

棧自我管理

堆,由項目腳本運行時的內存管理器(Mono或IL2CPP)管理。

e.垃圾回收

棧能夠實現內存的自我管理

堆的內存管理需要通過垃圾回收機制來實現。Unity的垃圾回收使用Boehm GC演算法,是非代數和非壓縮的

「非代數」是指必須掃描整個託管堆,因此在執行收集傳遞時必須掃描整個堆,因此其性能因堆的大小擴展而降低。

「非壓縮」是指當釋放的內存產生的間隙不會消失。也就是說對象被銷毀,內存被釋放,這塊內存不會馬上收集成為空閑內存的一部分,這塊內存只能用來存儲比釋放對象相同或更小的數據,如果內存間隔太小,會產生內存碎片

3、一共有幾種類型的數據?

一共有四類

值類型。在C#中,所有從System.ValueType繼承的類型

引用類型。類、介面、委託、object對象、string、stringBuilder

指針。當我們把對象放到堆內存時,訪問該對象,就需要一個指向該對象的引用,也就是指針

指令。處理指令,比如變數聲明、數學運算、跳轉等

4、棧的主要功能是什麼?

棧的主要功能是跟蹤線程執行時的代碼指針的位置,以及被調用和返回的數據。

當調用函數時,會將函數的參數壓入線程棧,在方法內的局部變數也會壓入線程棧頂。方法執行結束後, 返回值被返回。

5、怎麼確定數據分配到了哪個內存區?怎麼確定數據分配在棧還是堆上?

兩個原則

a.值類型和指針總分配在被聲明的地方,即他們的分配與聲明的位置有關,聲明在哪兒就分配在哪兒

b.非空引用類型對象和所有裝箱值類型對象總是分配在堆內存上,如下圖所示

6、值類型一定分配在棧上嗎?

不對。

如果聲明在函數的局部變數,就分配到線程棧中;如果聲明在一個class類中,就分配在堆內存中

7、垃圾回收是如何工作的?垃圾回收的執行過程是什麼樣的?

a.掛起所有正在運行的線程

b.檢查堆上的每個對象

c.搜索當前對象的所有引用

d.沒有被引用的對象都是垃圾,被標記為可刪除

e.最後遍歷刪除被標記為可刪除的對象,釋放內存

8、什麼情況會觸發垃圾回收?

a.代碼需要在託管堆上分配內存時,但發現可分配的空間不足的情況下會觸發GC

b.手動代碼調用觸發GC

c.unity不定期的觸發GC

9、內存碎片的現象會造成什麼問題?

堆內存是一塊連續的內存地址,清理其中垃圾之後,就會造成內存碎片。

內存間隔太小不夠放新的對象

一個託管堆雖然總空間量已經很大了,但是在這個空間里的內存間隔找不到連續空間來存儲新的對象

10、unity什麼時候會給堆內存擴容?

如果unity發現託管堆的內存不夠分配了,會先進行GC,如果GC之後,發現還是不夠分配,就進行託管堆的擴容(擴展),堆擴展的具體數量取決於平台; 但是,大多數Unity平台的大小都是託管堆的兩倍。

具體可參考下面的流程圖

11、託管堆上給變數分配內存的流程是?

12、如何用Unity CPU Profiler查看特定幀在託管堆上分配的內存數量?

Unity的CPU Profiler,概述中有一個「GC Alloc」列,顯示特定幀中託管堆上分配的位元組數

13、從代碼上有哪些寫法是可以避免內存碎片產生的?(持續更新)

a.不要在頻繁調用的函數中反覆進行堆內存分配

在像Update()和LateUpdate()這種每幀都調用的函數,可以判斷值變化了才調用某個會有堆內存分配的函數,或者計時器到了才調用某個函數

b.清空而不是創建集合

創建新的集合(比如:數組,字典,鏈表等集合類數據)會導致託管堆上的內存分配 , 如果發現在代碼中不止一次地創建新集合,那麼我們應該緩存引用到的集合,並使用Clear()清空其內容,而不是重複調用new()

c.儘可能避免C#中的閉包。在性能敏感的代碼中應該儘可能的減少匿名方法和方法引用的使用,尤其是在基於每幀執行的代碼中。

匿名方法要求該方法能夠訪問方法範圍之外的變數狀態,因此已成為閉包。於是C#通過生成一個匿名類,可以保留閉包所需的外部範圍變數。

當執行閉包需要實例化其生成的類的副本,並且所有類都是C#中的引用類型,所以執行閉包需要在託管堆上分配對象。

d.裝箱

裝箱是Unity中非常常見的非預期的臨時內存分配原因之一。只要將值類型值用作引用類型,就會發生這種情況

C#的IDE和編譯器通常不會發出有關裝箱的警告,即使它會導致意外的內存分配。這是因為C#語言是在假設小型臨時分配將由分代垃圾收集器和分配大小敏感的內存池有效處理的情況下開發的。雖然Unity的分配器確實使用不同的內存池進行小型和大型分配,但Unity的垃圾收集器是不是分代的,因此不能有效地掃除由裝箱生成的小的,頻繁的臨時分配。

在用Unity運行時編寫C#代碼時,應儘可能避免使用裝箱操作。

裝箱的一個常見原因是使用enum類型作為詞典的鍵。要解決這個問題,有必要編寫一個實現IEqualityComparer介面的自定義類,並將該類的實例指定為Dictionary的比較器

Unity 5.5中的C#編譯器升級顯著提高了Unity生成IL的能力。已經從foreach循環中消除了裝箱操作。消除了與foreach循環相關的內存開銷。

e.String相關

在C#中,String字元串是引用類型而不是值類型。C#中的字元串是不可變更的,其引用指向的值在創建後是不可被變更的。因此在創建或者丟棄字元串的時候,會造成託管堆內存分配。

推薦做法:

減少不必要的字元串創建,提前創建並持有緩存

減少不必要的字元串操作,比如常用的+。每次在對字元串進行操作的時候(例如運用字元串的」+」操作),unity會新建一個字元串用來存儲相加後的字元串。然後使之前的舊字元串被標記為廢棄,成為內存垃圾。

改用StringBuilder類 , StringBuilder就是專門設計用來創建字元串而不產生額外託管堆分配的類,而且可以避免字元串拼接產生垃圾

f.注意由於調用Unity的API所造成的堆內存分配

如果函數需要返回一個數組,則一個新的數組會被創建用作結果返回,簡單地緩存一個對數組的引用

函數gameobject.name或gameobject.tag,可以使用一個相關的聯合函數。

用Input.GetTouch()和Input.touchCount()來代替Input.touches或者用Physics.SphereCastNonAlloc()來代替Physics.SphereCastAll()

雖然一次訪問屬性的CPU成本不是很高,但在緊密循環內重複訪問,就可能不必要地擴展了託管堆。

14.unity的mono堆內存分配後會返還給系統嗎?

不會,目前Unity所使用的Mono版本存在一個很嚴重的問題,Mono的堆內存是只升不降。


閱讀

如何大幅優化NGUI的堆內存分配

blog.uwa4d.com/archives

github.com/sophiepeitho

Unity匿名函數的堆內存優化

blog.uwa4d.com/archives

參考

官方優化文檔

Best practice guides

blog.csdn.net/salvare/a


推薦閱讀:
相关文章