本文档主要是对Unity官方教程的个人理解与总结(其实以翻译记录为主:>)
仅作为个人学习使用,不得作为商业用途,欢迎转载,并请注明出处。
文章中涉及到的操作都是基于 Unity2017.3版本
参考链接: https://docs.unity3d.com/Manual/UnderstandingAutomaticMemoryManagement.html
When an object, string or array is created, the memory required to store it is allocated from a central pool called the heap. When the item is no longer in use, the memory it once occupied can be reclaimed and used for something else. In the past, it was typically up to the programmer to allocate and release these blocks of heap memory explicitly with the appropriate function calls. Nowadays, runtime systems like Unity’s Mono engine manage memory for you automatically. Automatic memory management requires less coding effort than explicit allocation/release and greatly reduces the potential for memory leakage (the situation where memory is allocated but never subsequently released).
当一个对象、字符串或数组被创建时,存储它所需的内存是从一个名为堆的中央池中分配的。当变量不再使用时,它曾经占用的内存可以被回收,并用于其他用途。在过去,通常由程序员根据适当的函数调用显式地分配和释放这些堆内存块。现在,像Unity的Mono引擎这样的运行时系统会自动为你管理内存。自动内存管理比显式分配/释放有更少的编码工作,并且极大地降低了内存泄漏(内存被分配但从未被释放的情况)。
When a function is called, the values of its parameters are copied to an area of memory reserved for that specific call. Data types that occupy only a few bytes can be copied very quickly and easily. However, it is common for objects, strings and arrays to be much larger and it would be very inefficient if these types of data were copied on a regular basis. Fortunately, this is not necessary; the actual storage space for a large item is allocated from the heap and a small “pointer” value is used to remember its location. From then on, only the pointer need be copied during parameter passing. As long as the runtime system can locate the item identified by the pointer, a single copy of the data can be used as often as necessary.
当一个函数被调用时,它的参数值被复制到一个特定调用而保留的内存区域。只占用几个字节的数据类型可以非常快速和容易地复制。然而,多个对象、字符串和数组的规模要大得多,如果这些类型的数据经常被复制,那么它将非常低效。幸运的是,这并不是必要的;一个大的变量的实际存储空间是从堆中分配的,一个小的“指针”值用来记住它的位置。从那时起,在参数传递过程中只需要复制指针。只要运行时系统能够定位指针标识的对象,就可以在必要时使用此指针的数据副本。
Types that are stored directly and copied during parameter passing are called value types. These include integers, floats, booleans and Unity’s struct types (eg, Color and Vector3). Types that are allocated on the heap and then accessed via a pointer are called reference types, since the value stored in the variable merely “refers” to the real data. Examples of reference types include objects, strings and arrays.
在参数传递过程中直接存储和复制的类型称为值类型。这些包括整数、浮点数、布尔值和Unity的结构类型(如颜色和Vector3)。被分配到堆上的类型,然后通过一个指针访问的类型称为引用类型,因为变量中存储的值仅仅是“引用”到真实的数据。参考类型的例子包括对象、字符串和数组。
The memory manager keeps track of areas in the heap that it knows to be unused. When a new block of memory is requested (say when an object is instantiated), the manager chooses an unused area from which to allocate the block and then removes the allocated memory from the known unused space. Subsequent requests are handled the same way until there is no free area large enough to allocate the required block size. It is highly unlikely at this point that all the memory allocated from the heap is still in use. A reference item on the heap can only be accessed as long as there are still reference variables that can locate it. If all references to a memory block are gone (ie, the reference variables have been reassigned or they are local variables that are now out of scope) then the memory it occupies can safely be reallocated.
内存管理器跟踪堆中已知但未使用的区域。当请求一个新的内存块时(例如,当一个对象被实例化时),管理者选择一个未使用的区域来分配区块,然后从已知的未使用的空间中删除分配的内存。随后的请求以相同的方式处理,直到没有足够大的空闲区域来分配所需的块大小。这时,从堆中分配的所有内存仍然在使用是非常不可能的。只有在仍然可以定位它的引用变量时,才可以访问堆上被引用的对象。如果所有对此内存块的引用都消失了(例如,引用变量已经被重新分配,或者局部变量现在超出了范围),那么它所占用的内存就可以安全地重新分配。
To determine which heap blocks are no longer in use, the memory manager searches through all currently active reference variables and marks the blocks they refer to as “live”. At the end of the search, any space between the live blocks is considered empty by the memory manager and can be used for subsequent allocations. For obvious reasons, the process of locating and freeing up unused memory is known as garbage collection (or GC for short).
为了确定哪些堆块不再被使用,内存管理器会搜索所有当前活动的引用变量,并标记它们称为“活着”的区块。在搜索的最后,内存管理器会认为这些”活着“块之间的任何空间都是空的,并且可以用于后续的分配。出于显而易见的原因,定位和释放未使用内存的过程称为垃圾回收(简称GC)。
Garbage collection is automatic and invisible to the programmer but the collection process actually requires significant CPU time behind the scenes. When used correctly, automatic memory management will generally equal or beat manual allocation for overall performance. However, it is important for the programmer to avoid mistakes that will trigger the collector more often than necessary and introduce pauses in execution.
垃圾回收是自动的,对程序员来说是不可见的,但是收集过程实际上需要在后台花费大量的CPU时间。当正确使用时,自动内存管理整体性能通常等于或超过手动分配。然而,对于程序员来说,避免错误会比 必要时频繁地触发回收并在执行中卡顿 更为重要。
There are some infamous algorithms that can be GC nightmares even though they seem innocent at first sight. Repeated string concatenation is a classic example:
有一些臭名昭著的算法可能是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;
}
The key detail here is that the new pieces don’t get added to the string in place, one by one. What actually happens is that each time around the loop, the previous contents of the line variable become dead - a whole new string is allocated to contain the original piece plus the new part at the end. Since the string gets longer with increasing values of i, the amount of heap space being consumed also increases and so it is easy to use up hundreds of bytes of free heap space each time this function is called. If you need to concatenate many strings together then a much better option is the Mono library’s System.Text.StringBuilder class.
这里的关键细节是,新块不会被添加到字符串中,一个接一个。实际发生的是,每当循环的时候,line 变量的前一个内容就会消失——一个全新的字符串被分配,内容包含原来的部分和最后的新部分。字符串随着i值的增加而变长,所以消耗的堆空间也会增加,所以每次调用这个函数时都很容易使用几百个字节的空闲堆空间。如果您需要将许多字符串连接在一起,那么更好的选择是Mono库的 System.Text.StringBuilder 类。
However, even repeated concatenation won’t cause too much trouble unless it is called frequently, and in Unity that usually implies the frame update. Something like:
然而,即使是重复的连接也不会造成太大的麻烦,除非它经常被调用,并且在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;
}
…will allocate new strings each time Update is called and generate a constant trickle of new garbage. Most of that can be saved by updating the text only when the score changes:
每次更新被调用时,都会分配新的字符串,并不断生成新垃圾。只有当分数发生变化时,才更新文本可以节省大部分内容:
//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;
}
}
Another potential problem occurs when a function returns an array value:
当一个函数返回一个数组值时,会出现另一个潜在的问题:
//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;
}
This type of function is very elegant and convenient when creating a new array filled with values. However, if it is called repeatedly then fresh memory will be allocated each time. Since arrays can be very large, the free heap space could get used up rapidly, resulting in frequent garbage collections. One way to avoid this problem is to make use of the fact that an array is a reference type. An array passed into a function as a parameter can be modified within that function and the results will remain after the function returns. A function like the one above can often be replaced with something like:
当创建一个填充值的新数组时,这种类型的函数非常简洁方便。然而,如果它被反复调用,那么每次都将分配新的内存。由于数组可以非常大,所以空闲堆空间可以快速地消耗掉,从而导致频繁的垃圾回收。避免这个问题的一种方法是利用数组是一种引用类型的事实。作为参数传递给函数的数组可以在该函数中进行修改,结果将保留在函数返回之后。像上面这样的函数通常可以用下面代替:
//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;
}
}
This simply replaces the existing contents of the array with new values. Although this requires the initial allocation of the array to be done in the calling code (which looks slightly inelegant), the function will not generate any new garbage when it is called.
这只是用新的值简单替换数组的现有内容。尽管这要求在调用代码时完成数组的初始分配(看起来有点不雅),但是当调用时,函数不会产生任何新的垃圾。
As mentioned above, it is best to avoid allocations as far as possible. However, given that they can’t be completely eliminated, there are two main strategies you can use to minimise their intrusion into gameplay:
如上所述,最好尽量避免分配内存。然而,考虑到它们不能被完全消除,你可以使用两种主要策略来减少它们对游戏的干扰:
Small heap with fast and frequent garbage collection
小堆,快速和频繁的垃圾回收
This strategy is often best for games that have long periods of gameplay where a smooth framerate is the main concern. A game like this will typically allocate small blocks frequently but these blocks will be in use only briefly. The typical heap size when using this strategy on iOS is about 200KB and garbage collection will take about 5ms on an iPhone 3G. If the heap increases to 1MB, the collection will take about 7ms. It can therefore be advantageous sometimes to request a garbage collection at a regular frame interval. This will generally make collections happen more often than strictly necessary but they will be processed quickly and with minimal effect on gameplay:
这一策略通常适用于那些占用长时间的游戏,在游戏中流畅的帧率是主要的关注点。像这样的游戏通常会频繁地分配小块,但是这些块只会在短时间内使用。在iOS上使用这个策略时,典型的堆大小大约是200 KB,在iPhone 3G上垃圾回收大约需要5 ms。如果堆增加到1 MB,那么这个集合将花费大约7 ms。因此,在一个常规的帧间隔中请求垃圾回收有时是有利的。这通常会使回收的发生频率超过必须回收频率,但是它们会被快速处理,并且对游戏的影响最小:
if (Time.frameCount % 30 == 0)
{
System.GC.Collect();
}
However, you should use this technique with caution and check the profiler statistics to make sure that it is really reducing collection time for your game.
但是,您应该谨慎地使用这项技术,并检查profiler 统计数据,以确保它确实减少了您的游戏的回收时间。
Large heap with slow but infrequent garbage collection
缓慢但不频繁的大堆垃圾回收
This strategy works best for games where allocations (and therefore collections) are relatively infrequent and can be handled during pauses in gameplay. It is useful for the heap to be as large as possible without being so large as to get your app killed by the OS due to low system memory. However, the Mono runtime avoids expanding the heap automatically if at all possible. You can expand the heap manually by preallocating some placeholder space during startup (ie, you instantiate a “useless” object that is allocated purely for its effect on the memory manager):
这种策略在游戏中效果最好,因为在游戏中,分配(因此,回收)相对较少,并且可以在游戏的暂停期间进行处理。对于堆来说,要尽可能大,但不要因为系统内存不足而导致应用程序被操作系统杀死,这是很有用的。然而,在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;
}
A sufficiently large heap should not get completely filled up between those pauses in gameplay that would accommodate a collection. When such a pause occurs, you can request a collection explicitly:
一个足够大的堆不应该在游戏运行时完全填满,而去调一次回收。当这样的暂停发生时,您可以显式地请求回收:
System.GC.Collect();
Again, you should take care when using this strategy and pay attention to the profiler statistics rather than just assuming it is having the desired effect.
同样,在使用这种策略时您应该小心,并注意分profiler 统计数据,而不是仅仅假设它达到预期的效果。
There are many cases where you can avoid generating garbage simply by reducing the number of objects that get created and destroyed. There are certain types of objects in games, such as projectiles, which may be encountered over and over again even though only a small number will ever be in play at once. In cases like this, it is often possible to reuse objects rather than destroy old ones and replace them with new ones.
在许多情况下,您可以通过减少被创建和销毁的对象的数量来避免生成垃圾。游戏中有某些类型的物体,比如抛射物,可能会一次又一次地遇到,即使只有一小部分会同时出现。在这种情况下,通常可以重用对象而不是销毁旧的对象,并将它们替换成新的对象。
Memory management is a subtle and complex subject to which a great deal of academic effort has been devoted. If you are interested in learning more about it then memorymanagement.org is an excellent resource, listing many publications and online articles. Further information about object pooling can be found on the Wikipedia page and also at Sourcemaking.com.
内存管理是一个精细而复杂的课题,大量的学术努力都有投入其中。如果您有兴趣了解更多信息,那么memorymanagement.org是一个优秀的资源,列出了许多出版物和在线文章。关于对象池的更多信息可以在 Wikipedia Page和Sourcemaking.com上找到。