當建立對象、字元串或數組時,存儲它所需的記憶體将從稱為堆的中央池中配置設定。當項目不再使用時,它曾經占用的記憶體可以被回收并用于别的東西。在過去,通常由程式員通過适當的函數調用明确地配置設定和釋放這些堆記憶體塊。如今,像Unity的Mono引擎這樣的運作時系統會自動為您管理記憶體。自動記憶體管理需要比顯式配置設定/釋放更少的編碼工作,并大大降低記憶體洩漏(記憶體被配置設定但從未随後釋放的情況)的可能性。
值類型和引用類型
當調用一個函數時,它的參數值将被複制到一個保留特定調用的記憶體區域。隻占用幾個位元組的資料類型可以非常快速友善地複制。然而,對象、字元串和數組要大得多,如果這些類型的資料被定期複制,那将是非常低效的。幸運的是,這是不必要的;大項目的實際存儲空間是從堆中配置設定的,一個小的“指針”值用來記住它的位置。從那時起,隻有指針在參數傳遞過程中需要被複制。隻要運作時系統能夠定位指針辨別的項,就可以經常使用資料的一個副本。
在參數傳遞期間直接存儲和複制的類型稱為值類型。這些包括整數,浮點數,布爾和Unity的結構類型(例如Color和Vector3)。配置設定在堆上然後通過指針通路的類型稱為引用類型,因為存儲在變量中的值僅僅是“引用”到真實資料。引用類型的示例包括對象,字元串和數組。
記憶體配置設定和垃圾收集
記憶體管理器跟蹤它知道未被使用的堆中的區域。當請求一個新的記憶體塊時(例如當一個對象被執行個體化時),管理器選擇一個未使用的區域,從中配置設定該塊,然後從已知的未使用的空間中移除配置設定的記憶體。後續請求以相同的方式處理,直到沒有足夠大的空閑區域配置設定所需的塊大小。在這一點上,從堆中配置設定的所有記憶體仍然在使用中是非常不可能的。隻要還存在可以找到它的引用變量,就隻能通路堆上的引用項。如果對記憶體塊的所有引用都消失了(即,引用變量已被重新配置設定,或者它們是現在超出範圍的局部變量),則它占用的記憶體可以安全地重新配置設定。
為了确定哪些堆塊不再被使用,記憶體管理器會搜尋所有目前活動的引用變量,并将它們所指的塊标記為
live
。在搜尋結束時,記憶體管理器認為這些
live
塊之間的任何空間都是空的,并且可用于後續配置設定。由于顯而易見的原因,定位和釋放未使用的記憶體的過程被稱為垃圾回收(或簡稱GC)。
優化
垃圾收集對程式員來說是自動的、不可見的,但是收集過程實際上需要大量的CPU時間。如果正确使用,自動記憶體管理通常會等于或擊敗手動配置設定的整體性能。但是,對于程式員來說,重要的是要避免那些比實際需要觸發更多次收集器和在執行中引入暫停的錯誤。有一些臭名昭著的算法,可能是GC噩夢,盡管他們乍一看是無辜的。重複字元串連接配接是一個典型的例子:
//C# script example
using UnityEngine;
using System.Collections;
public class ExampleScript : MonoBehaviour {
void ConcatExample(int[] intArray) {
string line = intArray[0].ToString();
for (i = 1; i < intArray.Length; i++) {
line += ", " + intArray[i].ToString();
}
return line;
}
}
//JS script example
function ConcatExample(intArray: int[]) {
var line = intArray[0].ToString();
for (i = 1; i < intArray.Length; i++) {
line += ", " + intArray[i].ToString();
}
return line;
}
這裡的關鍵細節是,新的部分不會被一個接一個地添加到字元串中。實際情況是,每次循環
line
變量的前一個内容都會變死——一個完整的新字元串被配置設定到包含原來的部分,再在最後加上新的部分。由于字元串随着
i
值的增加而變得更長,是以所消耗的堆空間數量也增加了,是以每次調用這個函數時都很容易消耗數百位元組的空閑堆空間。如果你需要連接配接多個字元串,那麼一個更好的選擇是Mono庫的System.Text.StringBuilder類。然而,即使反複連接配接也不會引起太多麻煩,除非它被頻繁調用,而在Unity中通常意味着幀更新。就像是:
//C# script example
using UnityEngine;
using System.Collections;
public class ExampleScript : MonoBehaviour {
public GUIText scoreBoard;
public int score;
void Update() {
string scoreText = "Score: " + score.ToString();
scoreBoard.text = scoreText;
}
}
//JS script example
var scoreBoard: GUIText;
var score: int;
function Update() {
var scoreText: String = "Score: " + score.ToString();
scoreBoard.text = scoreText;
}
...每次調用Update将配置設定新字元串,并不斷生成的新垃圾。大多數情況下,隻有當分數變化時才更新文本:
//C# script example
using UnityEngine;
using System.Collections;
public class ExampleScript : MonoBehaviour {
public GUIText scoreBoard;
public string scoreText;
public int score;
public int oldScore;
void Update() {
if (score != oldScore) {
scoreText = "Score: " + score.ToString();
scoreBoard.text = scoreText;
oldScore = score;
}
}
}
//JS script example
var scoreBoard: GUIText;
var scoreText: String;
var score: int;
var oldScore: int;
function Update() {
if (score != oldScore) {
scoreText = "Score: " + score.ToString();
scoreBoard.text = scoreText;
oldScore = score;
}
}
當函數傳回數組值時,會發生另一個潛在的問題:
//C# script example
using UnityEngine;
using System.Collections;
public class ExampleScript : MonoBehaviour {
float[] RandomList(int numElements) {
var result = new float[numElements];
for (int i = 0; i < numElements; i++) {
result[i] = Random.value;
}
return result;
}
}
//JS script example
function RandomList(numElements: int) {
var result = new float[numElements];
for (i = 0; i < numElements; i++) {
result[i] = Random.value;
}
return result;
}
當建立一個充滿值的新數組時,這種函數非常優雅和友善。但是,如果反複調用,那麼每次都會配置設定新的記憶體。由于數組可能非常大,可用空間可能會迅速消耗,進而導緻垃圾收集頻繁。避免這個問題的一個方法是利用數組是引用類型的事實。作為參數傳遞給函數的數組可以在該函數内修改,結果将在函數傳回後保留。
像上面這樣的功能通常可以被替換成:
//C# script example
using UnityEngine;
using System.Collections;
public class ExampleScript : MonoBehaviour {
void RandomList(float[] arrayToFill) {
for (int i = 0; i < arrayToFill.Length; i++) {
arrayToFill[i] = Random.value;
}
}
}
//JS script example
function RandomList(arrayToFill: float[]) {
for (i = 0; i < arrayToFill.Length; i++) {
arrayToFill[i] = Random.value;
}
}
這隻是用新值替換數組的現有内容。雖然這需要在調用代碼中完成數組的初始配置設定(這似乎有些不雅),但是在調用該函數時不會産生任何新的垃圾。
主動請求垃圾收集
如上所述,最好盡量避免配置設定。然而,鑒于它們不能被完全消除,您可以使用兩種主要政策來最大限度地減少其入侵遊戲:
小堆垃圾收集快速可頻繁收集
這個政策通常最适合長期遊戲的遊戲,其中平滑的幀速率是主要的關注點。這樣的遊戲通常會頻繁地配置設定小塊,但這些塊将僅在短時間内使用。在iOS上使用此政策時,典型的堆大小約為200KB,iPhone 3G上的垃圾收集大約需要5ms。如果堆增加到1MB,則收集大約需要7ms。是以,有時候可以以規則的幀間隔請求垃圾回收。這通常會使垃圾收集發生的次數比嚴格的需要的更多,但是它們将被快速處理,對遊戲的影響最小:
if (Time.frameCount % 30 == 0)
{
System.GC.Collect();
}
但是,您應該謹慎使用此技術,并檢查profiler統計資訊,以確定它真正減少了遊戲的收集時間。
大堆垃圾收集緩慢且不可頻繁收集
這個政策對于配置設定(和是以收集)相對不頻繁并可以在遊戲暫停期間處理的遊戲最适用。對于堆來說,盡可能大,而不是因為系統記憶體太少而導緻作業系統殺死你的應用程式。但是,如果可能,Mono運作時會自動避免擴充堆。您可以通過在啟動期間預先配置設定一些占位符空間來手動擴充堆(即,您執行個體化一個純粹用于對記憶體管理器産生影響的“無用”對象):
//C# script example
using UnityEngine;
using System.Collections;
public class ExampleScript : MonoBehaviour {
void Start() {
var tmp = new System.Object[1024];
// make allocations in smaller blocks to avoid them to be treated in a special way, which is designed for large blocks
for (int i = 0; i < 1024; i++)
tmp[i] = new byte[1024];
// release reference
tmp = null;
}
}
//JS script example
function Start() {
var tmp = new System.Object[1024];
// make allocations in smaller blocks to avoid them to be treated in a special way, which is designed for large blocks
for (var i : int = 0; i < 1024; i++)
tmp[i] = new byte[1024];
// release reference
tmp = null;
}
一個足夠大的堆不應該在遊戲中的暫停期間完全被填滿,這樣可以容納一次收集。當發生這樣的暫停時,您可以顯式地請求垃圾收集:
System.GC.Collect();
同樣,在使用此政策時應該小心,并注意Profiler統計資料,而不是僅僅假定它具有所期望的效果。
可重複使用的對象池
很多情況下,隻要減少建立和銷毀對象的數量,就可以避免生成垃圾。遊戲中存在着某些類型的物體,如抛射體,盡管一次隻會有少量的物體在遊戲中,但它們可能會被反複地遇到。在這種情況下,常常可以重用對象,而不是破壞舊對象,并用新的對象替換它們。
更多資訊
記憶體管理是一個微妙而複雜的課題,它已經投入了大量的學術研究。如果您有興趣了解更多資訊,那麼memorymanagement.org是一個很好的資源,列出了許多出版物和線上文章。有關對象池的更多資訊可以在維基百科頁面和Sourcemaking.com上找到。
原文連結:Understanding Automatic Memory Management
本文作者: Sheh偉偉
本文連結: http://davidsheh.github.io/2017/07/13/「翻譯」了解Unity的自動記憶體管理/
版權聲明: 本部落格所有文章除特别聲明外,均采用 CC BY-NC-SA 3.0 許可協定。轉載請注明出處!
作者:Sheh偉偉
出處:http://www.cnblogs.com/davidsheh/
本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接配接,否則保留追究法律責任的權利.