Unity記憶體優化 —— GC詳解
- 前言
- 簡介
- Unity記憶體管理機制簡介
- 堆棧記憶體配置設定和回收機制
- 堆記憶體配置設定和回收機制
- 垃圾回收時的操作
- 何時會觸發垃圾回收
- GC操作帶來的問題
- 分析GC帶來的問題
- 分析堆記憶體的配置設定
- 接下來用兩個簡單案例闡述值類型和引用類型的回收機制
- 利用profiler window 來檢測堆記憶體配置設定:
- 降低GC的影響的方法
- 減少記憶體垃圾的數量
- 清除連結清單
- 對象池
- 定時執行GC操作
前言
感謝部落客提供思路,内容修改自:https://www.cnblogs.com/zblade/p/6445578.html
簡介
- 在遊戲運作的時候,資料主要存儲在記憶體中,當遊戲的資料在不需要的時候,存儲目前資料的記憶體就可以被回收以再次使用。記憶體垃圾是指目前廢棄資料所占用的記憶體,垃圾回收(GC)是指将廢棄的記憶體重新回收再次使用的過程。
- Unity中将垃圾回收當作記憶體管理的一部分,如果遊戲中廢棄資料占用記憶體較大,則遊戲的性能會受到極大影響,此時垃圾回收會成為遊戲性能的一大障礙點。
Unity記憶體管理機制簡介
要想了解垃圾回收如何工作以及何時被觸發,我們首先需要了解unity的記憶體管理機制。Unity主要采用自動記憶體管理的機制,開發時在代碼中不需要詳細地告訴unity如何進行記憶體管理,unity内部自身會進行記憶體管理。這和使用C++開發需要随時管理記憶體相比,有一定的優勢,當然帶來的劣勢就是需要随時關注記憶體的增長,不要讓遊戲在手機上跑“飛”了。
unity的自動記憶體管理可以了解為以下幾個部分:
1)unity内部有兩個記憶體管理池:堆記憶體和堆棧記憶體。堆棧記憶體(stack)主要用來存儲較小的和短暫的資料,堆記憶體(heap)主要用來存儲較大的和存儲時間較長的資料。
2)unity中的變量隻會在堆棧或者堆記憶體上進行記憶體配置設定,變量要麼存儲在堆棧記憶體上,要麼處于堆記憶體上。
3)隻要變量處于激活狀态,則其占用的記憶體會被标記為使用狀态,則該部分的記憶體處于被配置設定的狀态。
4)一旦變量不再激活,則其所占用的記憶體不再需要,該部分記憶體可以被回收到記憶體池中被再次使用,這樣的操作就是記憶體回收。處于堆棧上的記憶體回收及其快速,處于堆上的記憶體并不是及時回收的,此時其對應的記憶體依然會被标記為使用狀态。
5) 垃圾回收主要是指堆上的記憶體配置設定和回收,unity中會定時對堆記憶體進行GC操作。
在了解了GC的過程後,下面詳細了解堆記憶體和堆棧記憶體的配置設定和回收機制的差别。
堆棧記憶體配置設定和回收機制
堆棧上的記憶體配置設定和回收十分快捷簡單,因為堆棧上隻會存儲短暫的或者較小的變量。記憶體配置設定和回收都會以一種順序和大小可控制的形式進行。
堆棧的運作方式就像stack: 其本質隻是一個資料的集合,資料的進出都以一種固定的方式運作。正是這種簡潔性和固定性使得堆棧的操作十分快捷。當資料被存儲在堆棧上的時候,隻需要簡單地在其後進行擴充。當資料失效的時候,隻需要将其從堆棧上移除。
堆記憶體配置設定和回收機制
堆記憶體上的記憶體配置設定和存儲相對而言更加複雜,主要是堆記憶體上可以存儲短期較小的資料,也可以存儲各種類型和大小的資料。其上的記憶體配置設定和回收順序并不可控,可能會要求配置設定不同大小的記憶體單元來存儲資料。
堆上的變量在存儲的時候,主要分為以下幾步:
1)首先,unity檢測是否有足夠的閑置記憶體單元用來存儲資料,如果有,則配置設定對應大小的記憶體單元;
2)如果沒有足夠的存儲單元,unity會觸發垃圾回收來釋放不再被使用的堆記憶體。這步操作是一步緩慢的操作,如果垃圾回收後有足夠大小的記憶體單元,則進行記憶體配置設定。
3)如果垃圾回收後并沒有足夠的記憶體單元,則unity會擴充堆記憶體的大小,這步操作會很緩慢,然後配置設定對應大小的記憶體單元給變量。
堆記憶體的配置設定有可能會變得十分緩慢,特别是在需要垃圾回收和堆記憶體需要擴充的情況下,通常需要減少這樣的操作次數。
垃圾回收時的操作
當堆記憶體上一個變量不再處于激活狀态的時候,其所占用的記憶體并不會立刻被回收,不再使用的記憶體隻會在GC的時候才會被回收。
每次運作GC的時候,主要進行下面的操作:
1)GC會檢查堆記憶體上的每個存儲變量;
2)對每個變量會檢測其引用是否處于激活狀态;
3)如果變量的引用不再處于激活狀态,則會被标記為可回收;
4)被标記的變量會被移除,其所占有的記憶體會被回收到堆記憶體上。
GC操作是一個極其耗費的操作,堆記憶體上的變量或者引用越多則其運作的操作會更多,耗費的時間越長。
何時會觸發垃圾回收
主要有三個操作會觸發垃圾回收:
1) 在堆記憶體上進行記憶體配置設定操作而記憶體不夠的時候都會觸發垃圾回收來利用閑置的記憶體;
2) GC會自動的觸發,不同平台運作頻率不一樣;
3) GC可以被強制執行。
特别是在堆記憶體上進行記憶體配置設定時記憶體單元不足夠的時候,GC會被頻繁觸發,這就意味着頻繁在堆記憶體上進行記憶體配置設定和回收會觸發頻繁的GC操作。
GC操作帶來的問題
在了解GC在unity記憶體管理中的作用後,我們需要考慮其帶來的問題。最明顯的問題是GC操作會需要大量的時間來運作,如果堆記憶體上有大量的變量或者引用需要檢查,則檢查的操作會十分緩慢,這就會使得遊戲運作緩慢。其次GC可能會在關鍵時候運作,例如在CPU處于遊戲的性能運作關鍵時刻,此時任何一個額外的操作都可能會帶來極大的影響,使得遊戲幀率下降。
另外一個GC帶來的問題是堆記憶體的碎片化。當一個記憶體單元從堆記憶體上配置設定出來,其大小取決于其存儲的變量的大小。當該記憶體被回收到堆記憶體上的時候,有可能使得堆記憶體被分割成碎片化的單元。也就是說堆記憶體總體可以使用的記憶體單元較大,但是單獨的記憶體單元較小,在下次記憶體配置設定的時候不能找到合适大小的存儲單元,這也會觸發GC操作或者堆記憶體擴充操作。
堆記憶體碎片會造成兩個結果,一個是遊戲占用的記憶體會越來越大,一個是GC會更加頻繁地被觸發。
分析GC帶來的問題
GC操作帶來的問題主要表現為幀率運作低,性能間歇中斷或者降低。如果遊戲有這樣的表現,則首先需要打開unity中的profiler window來确定是否是GC造成。
了解如何運用profiler window,可以參考:https://unity3d.com/cn/learn/tutorials/temas/performance-optimization/diagnosing-performance-problems-using-profiler-window?playlist=44069
如果遊戲确實是由GC造成的,可以繼續閱讀下面的内容。
分析堆記憶體的配置設定
如果GC造成遊戲的性能問題,我們需要知道遊戲中的哪部分代碼會造成GC,記憶體垃圾在變量不再激活的時候産生,是以首先我們需要知道堆記憶體上配置設定的是什麼變量。
接下來用兩個簡單案例闡述值類型和引用類型的回收機制
該值類型在函數結束之後就會回收
void ExampleFunciton()
{
int localInt = 5;
}
該引用類型在觸發GC的時候才會回收
void ExampleFunction()
{
List localList = new List();
}
利用profiler window 來檢測堆記憶體配置設定:
我們可以在profier window中檢查堆記憶體的配置設定操作:在CPU usage分析視窗中,我們可以檢測任何一幀cpu的記憶體配置設定情況。其中一個選項是GC Alloc,通過分析其來定位是什麼函數造成大量的堆記憶體配置設定操作。一旦定位該函數,我們就可以分析解決其造成問題的原因進而減少記憶體垃圾的産生。現在Unity5.5的版本,還提供了deep profiler的方式深度分析GC垃圾的産生。
降低GC的影響的方法
大體上來說,我們可以通過三種方法來降低GC的影響:
1)減少GC的運作次數;
2)減少單次GC的運作時間;
3)将GC的運作時間延遲,避免在關鍵時候觸發,比如可以在場景加載的時候調用GC
似乎看起來很簡單,基于此,我們可以采用三種政策:
1)對遊戲進行重構,減少堆記憶體的配置設定和引用的配置設定。更少的變量和引用會減少GC操作中的檢測個數進而提高GC的運作效率。
2)降低堆記憶體配置設定和回收的頻率,尤其是在關鍵時刻。也就是說更少的事件觸發GC操作,同時也降低堆記憶體的碎片化。
3)我們可以試着測量GC和堆記憶體擴充的時間,使其按照可預測的順序執行。當然這樣操作的難度極大,但是這會大大降低GC的影響。
減少記憶體垃圾的數量
減少記憶體垃圾主要可以通過一些方法來減少(緩存):
如果在代碼中反複調用某些造成堆記憶體配置設定的函數但是其傳回結果并沒有使用,這就會造成不必要的記憶體垃圾,我們可以緩存這些變量來重複利用,這就是緩存。
例如下面的代碼每次調用的時候就會造成堆記憶體配置設定,主要是每次都會配置設定一個新的數組:
void OnTriggerEnter(Collider other)
{
Renderer[] allRenderers = FindObjectsOfType<Renderer>();
ExampleFunction(allRenderers);
}
對比下面的代碼,隻會生産一個數組用來緩存資料,實作反複利用而不需要造成更多的記憶體垃圾:
private Renderer[] allRenderers;
void Start()
{
allRenderers = FindObjectsOfType<Renderer>();
}
void OnTriggerEnter(Collider other)
{
ExampleFunction(allRenderers);
}
不要反複對記憶體進行配置設定,如下錯誤的方式
void Update()
{
ExampleGarbageGenerationFunction(transform.position.x);
}
改變之後
private float previousTransformPositionX;
void Update()
{
float transformPositionX = transform.position.x;
if(transfromPositionX != previousTransformPositionX)
{
ExampleGarbageGenerationFunction(transformPositionX);
previousTransformPositionX = trasnformPositionX;
}
}
清除連結清單
在堆記憶體上進行連結清單的配置設定的時候,如果該連結清單需要多次反複的配置設定,我們可以采用連結清單的clear函數來清空連結清單進而替代反複多次的建立配置設定連結清單。
void Update()
{
List myList = new List();
PopulateList(myList);
}
通過改進,我們可以将該連結清單隻在第一次建立或者該連結清單必須重新設定的時候才進行堆記憶體配置設定,進而大大減少記憶體垃圾的産生:
private List myList = new List();
void Update()
{
myList.Clear();
PopulateList(myList);
}
對象池
即便我們在代碼中盡可能地減少堆記憶體的配置設定行為,但是如果遊戲有大量的對象需要産生和銷毀依然會造成GC。對象池技術可以通過重複使用對象來降低堆記憶體的配置設定和回收頻率。對象池在遊戲中廣泛的使用,特别是在遊戲中需要頻繁的建立和銷毀相同的遊戲對象的時候,例如槍的子彈這種會頻繁生成和銷毀的對象。
定時執行GC操作
如果我們知道堆記憶體在被配置設定後并沒有被使用,我們希望可以主動地調用GC操作,或者在GC操作并不影響遊戲體驗的時候(例如場景切換的時候),我們可以主動的調用GC操作:
System.GC.Collect()