介绍
当咱们的游戏运行时,它使用内存来存储数据。当再也不须要该数据时,存储该数据的内存将被释放,以即可以重用。垃圾是用来存储数据但再也不使用的内存的术语。垃圾回收是该内存再次可用以进行重用的进程的名称。
Unity使用垃圾回收做为管理内存的一部分。若是垃圾回收发生得太频繁或者有太多工做要作,咱们的游戏可能会表现不佳,这意味着垃圾回收是致使性能问题的常见缘由。
在本文中,咱们将了解垃圾回收如何工做的,何时发生垃圾回收,以及如何有效地使用内存,从而最小化垃圾回收对游戏的影响。
诊断垃圾回收的问题
垃圾回收致使的性能问题能够表现为帧率低、性能不稳定或间歇性冻结。然而,其余问题也会引发相似的症状。若是咱们的游戏有这样的性能问题,咱们应该作的第一件事就是使用Unity的Profiler窗口来肯定咱们看到的问题是否真的是因为垃圾回收形成的。
要了解如何使用Profiler窗口查找性能问题的缘由,请查阅
这一篇教程。
Unity内存管理简介
要理解垃圾回收是如何工做的,以及垃圾回收什么时候发生,咱们必须首先理解Unity中内存的使用是如何工做的。首先,咱们必须理解Unity在运行它本身的核心引擎代码和运行咱们编写的代码时使用了不一样的方法。
Unity在运行本身的核心Unity引擎代码时管理内存的方式叫作手动内存管理。这意味着核心引擎代码必须显式地声明如何使用内存。手动内存管理不使用垃圾回收,本文将再也不深刻讨论。
Unity在运行咱们的代码时管理内存的方式叫作自动内存管理。这意味着咱们的代码不须要显式地告诉Unity如何以一种详细的方式管理内存。Unity帮咱们解决了这个问题。
在最基本的层面上,Unity中的自动内存管理是这样工做的:
- Unity能够访问两个内存池:栈(stack)和堆(heap,也称为托管堆)。栈用于短时间存储小块数据,堆用于长期存储和大块数据。
- 当一个变量被建立时,Unity会从栈或堆中请求一个内存块。
- 只要变量在做用域内(仍然可由代码访问),分配给它的内存就会一直使用。咱们说这个内存已经分配了。咱们将栈内存中保存的变量描述为栈上的对象,将堆内存中保存的变量描述为堆上的对象。
- 当变量超出做用域时,内存再也不须要,能够将其返回到它所来自的池中。当内存被返回到它的池时,咱们说内存已被释放。栈中内存在它引用的变量超出做用域时当即释放。可是,堆中的内存此时没有释放,而且仍然处于已分配的状态,即便它引用的变量超出了做用域。
- 垃圾回收器标识和释放未使用的堆内存。垃圾回收器按期运行以清理堆。
如今咱们已经了解了事件流,接下来咱们进一步了解栈内存分配和释放与堆分配和释放的区别。
栈分配和释放期间发生了什么?
栈分配和释放是快速和简单的。这是由于栈仅用于短期内存储小数据。分配和回收老是以可预测的顺序发生,而且具备可预测的大小。
栈的工做方式相似于栈数据类型:它是元素的简单集合,在本例中是内存块,其中元素只能按照严格的顺序添加和删除。这种简单性和严格性使得它如此快速:当一个变量存储在栈上时,它的内存只是从栈的“末端”分配的。当栈变量超出做用域时,用于存储该变量的内存将当即返回栈以供重用内存。
堆分配期间发生了什么?
堆分配比栈分配复杂得多。这是由于堆能够用来存储长期和短时间数据,以及许多不一样类型和大小的数据。分配和回收并不老是按照可预测的顺序发生,可能须要许多不一样大小的内存块。
建立变量时,执行如下步骤:
- 首先,Unity必须检查堆中是否有足够的空闲内存。若是堆中有足够的空闲内存,则为变量分配内存。
- 若是堆中没有足够的空闲内存,Unity会触发垃圾回收器,试图释放未使用的堆内存。这多是一个缓慢的操做。若是堆中如今有足够的空闲内存,则分配变量的内存。
- 若是垃圾回收后堆中没有足够的空闲内存,Unity会增长堆中的内存容量。这多是一个缓慢的操做。而后分配变量的内存。
堆分配可能很慢,特别是在必须运行垃圾回收器和必须扩展堆的状况下。
垃圾回收期间发生了什么?
当堆变量超出做用域时,用于存储它的内存不会当即释放。未使用的堆内存仅在垃圾回收器运行时释放。
- 垃圾回收器检查堆上的每一个对象。
- 垃圾回收器搜索全部当前对象引用,以肯定堆上的对象是否仍在做用域内。
- 任何再也不在做用域中的对象都被标记为删除。
- 删除标记的对象,并将分配给它们的内存返回堆中。
垃圾回收是一项耗性能的操做。堆上的对象越多,它必须作的工做就越多,代码中的对象引用越多,它必须作的工做就越多。
垃圾回收何时发生?
有三种状况会致使垃圾回收器运行:
- 每当请求使用堆中的空闲内存没法完成堆分配时,垃圾回收器就会运行。
- 垃圾回收器不时自动运行(尽管频率随平台而变化)。
- 垃圾回收器能够强制手动运行。
垃圾回收是一种常见的操做。每当没法从可用堆内存中完成堆分配时,就会触发垃圾回收器,这意味着频繁的堆分配和回收会致使频繁的垃圾回收。
垃圾回收的问题
如今咱们已经了解了垃圾回收在Unity内存管理中的做用,咱们能够考虑可能发生的问题的类型。
最明显的问题是垃圾回收器可能须要至关长的事件来运行。若是垃圾回收器在堆上有不少对象和/或有对象引用要检查,那么检查全部这些对象的过程可能会很慢。这可能致使咱们的游戏卡顿或者缓慢。
另外一个问题是垃圾回收器可能在不合适的时间运行。若是CPU已经在游戏的性能关键部分努力运行着,那么即便垃圾回收带来的少许额外开销也会致使帧率降低和性能显著变化。
另外一个不太明显的问题是堆碎片。当从堆中分配内存时,根据必须存储的数据的大小,内存以不一样大小的块从空闲空间中获取。当这些内存块被返回到堆中时,堆能够被分割成许多由分配的块分隔的小的空闲块。这意味着,尽管空闲内存的容量可能很高,可是咱们没法在不运行垃圾回收器和/或扩展堆的状况下分配内存块,由于现有的块都不够大。
碎片堆有两个后果。第一,咱们的游戏内存使用量将高于它所须要的水平,第二,垃圾回收器将运行得更频繁。有关堆碎片得更详细讨论,请参考
这一篇文章。
发现堆分配
若是咱们知道垃圾回收在咱们的游戏中形成了问题,咱们就须要知道代码的哪些部分正在生成垃圾。垃圾是在堆变量超出做用域时生成的,所以首先咱们须要知道是什么缘由致使在堆上分配变量。
在栈和堆上分配了什么?
在Unity中,值类型的局部变量被分配到栈上,其余的变量被分配到堆上。若是你不肯定Unity中值类型和引用类型之间的区别,请参阅
本教程。
下面的代码是栈分配的一个示例,由于变量localInt既是本地的又是值类型的。为该变量分配的内存将在该函数完成运行后当即从栈中释放。
void ExampleFunction()
{
int localInt = 5;
}
下面的代码是堆分配的一个示例,由于变量localList是本地的,可是是引用类型。为该变量分配的内存将在垃圾回收器运行时回收。
void ExampleFunction()
{
List localList = new List();
}
使用Profiler窗口查找堆分配
咱们能够在Profiler窗口中看到代码建立的堆分配。
在选择了CPU usage Profiler以后,咱们能够在Profiler窗口的底部选择任何帧来查看关于该帧的CPU使用率数据。其中一系列数据称为GC alloc。这一列显示在该帧中进行的堆分配。若是咱们选择列标题,咱们能够经过这个统计数据对数据进行排序,这样就很容易看到游戏中哪些函数致使了最多的堆分配。一旦咱们知道哪一个函数致使堆分配,就能够检查这个函数。
一旦咱们知道函数中的哪些代码致使生成垃圾,咱们就能够决定如何解决这个问题并最小化生成垃圾数量。
减小垃圾回收的影响
通常来讲,咱们能够经过如下三种方式减小垃圾回收对游戏的影响:
- 咱们能够减小垃圾回收器运行的时间。
- 咱们能够减小垃圾回收器运行的频率。
- 咱们能够故意触发垃圾回收器,使其在性能不重要的时候运行(例如在加载屏幕期间)。
考虑到这一点,有三种策略能够帮助咱们:
- 咱们能够阻止咱们游戏,这样咱们就有更少的堆分配和更少的对象引用。堆上的对象越少,要检查的引用越少,这意味着在触发垃圾回收时,运行垃圾回收所需的时间越少。
- 咱们能够减小堆分配和回收的频率,特别是在性能关键时候。更少的分配和回收意味着触发垃圾回收的状况更少。这也下降了堆碎片的风险。
- 咱们能够尝试计时垃圾回收和堆扩展,以便在可预测和方便的时间进行。这是一种更困难、更不可靠的方法,可是若是将其做为总体内存管理策略的一部分使用,则能够减小垃圾回收的影响。
减小垃圾的建立
让咱们研究一些技术,它们将帮助咱们减小代码生成的垃圾数量。
缓存
若是咱们的代码重复调用致使堆分配的函数,而后丢弃结果,这会产生没必要要的垃圾。相反,咱们应该存储对这些对象的引用并重用它们。这种技术称为缓存。
在下面的示例中,每次调用代码时,代码都会致使堆分配。这是由于建立了一个新数组。
void OnTriggerEnter(Collider other)
{
Renderer[] allRenderers = FindObjectsOfType<Renderer>();
ExampleFunction(allRenderers);
}
下面的代码只会致使一个堆分配,由于数组只会建立和填充一次,而后缓存。缓存的数组能够一次又一次地重用,而不会产生更多的垃圾。
private Renderer[] allRenderers;
void Start()
{
allRenderers = FindObjectsOfType<Renderer>();
}
void OnTriggerEnter(Collider other)
{
ExampleFunction(allRenderers);
}
不要在频繁调用的函数中进行分配
若是咱们必须在Monobehaviour分配堆内存,最糟糕的状况是在频繁运行的函数中。例如,Update()和LateUpdate()在每一帧中调用一次,所以若是咱们的代码在这里生成垃圾,那么它会很快地累积起来。若是可能,咱们应该考虑在Start()或Awake()中缓存对对象的引用,或者确保只在须要时才运行致使分配的代码。
void Update()
{
ExampleGarbageGeneratingFunction(transform.position.x);
}
经过一个简单的更改,咱们如今确保仅在transform.position.x的值已经改变的状况下才调用分配函数。咱们如今只在须要的时候进行堆分配,而不是每一帧中进行。
private float previousTransformPositionX;
void Update()
{
float transformPositionX = transform.position.x;
if (transformPositionX != previousTransformPositionX)
{
ExampleGarbageGeneratingFunction(transformPositionX);
previousTransformPositionX = transformPositionX;
}
}
减小Update()中生成的垃圾的另外一种技术是使用计时器。这适用于当咱们生成垃圾的代码必须按期运行,但不必定是每一帧。
在下面的示例代码中,生成垃圾的函数每帧运行一次:
void Update()
{
ExampleGarbageGeneratingFunction();
}
在下面的代码中,咱们使用计时器来确保生成垃圾的函数每秒运行一次。
private float timeSinceLastCalled;
private float delay = 1f;
void Update()
{
timeSinceLastCalled += Time.deltaTime;
if (timeSinceLastCalled > delay)
{
ExampleGarbageGeneratingFunction();
timeSinceLastCalled = 0f;
}
}
对频繁运行的代码进行这样的小更改时,能够大大减小生成的垃圾数量。
清除集合
建立新的集合会致使堆上的分配。若是咱们发如今代码中不止一次地建立新集合,咱们应该缓存对该集合的引用,并使用Clear()清空其内容,而不是重复调用new。
在下面的示例中,每次使用new时都会发生新的堆分配。
void Update()
{
List myList = new List();
PopulateList(myList);
}
在下面的示例中,分配仅在建立集合或必须在幕后调整集合大小时发生。这大大减小了生成的垃圾数量。
private List myList = new List();
void Update()
{
myList.Clear();
PopulateList(myList);
}
对象池
即便咱们在代码中减小了分配,可是若是咱们在运行时建立和销毁大量对象,咱们仍然可能会遇到垃圾回收的问题。对象池是一种经过重用对象而不是重复建立和销毁 对象来减小分配和回收的技术。对象池在游戏中应用普遍,最适合咱们频繁生成和销毁类似对象的状况;例如,从枪里射出子弹时。
关于对象池的完整说明超出了本文的范围,但它确实是一种有用的技术,值得学习。Unity学习网站上关于对象池的教程
在这里。
没必要要的堆分配的常见缘由
咱们知道,本地的、值类型的变量是在栈上分配的,其余的都是在堆上分配的。然而,在许多状况下,堆分配可能会让咱们大吃一惊。让咱们看看形成没必要要堆分配的一些常见缘由,并考虑如何最好地减小这些缘由。
字符串(strings)
在C#中,字符串是引用类型而不是值类型,即便它们彷佛保存了字符串的“值”。这意味着建立和丢弃字符串都会产生垃圾。因为字符串在不少代码中都是经常使用的,因此这些垃圾其实是能够累加的。
C#中的字符串也是不可变的,这意味着它们的值在首次建立以后不能更改。每当咱们操做一个字符串(例如,经过使用+运算符链接两个字符串),Unity就会建立一个保存更新值得新字符串并丢弃旧字符串。这就产生了垃圾。
咱们能够遵循一些简单得规则来将字符串中得垃圾降到最低。让咱们考虑这些规则,而后看一个如何应用它们的示例。
- 咱们应该减小没必要要的字符串建立。若是咱们不止一次使用相同得字符串值,咱们应该建立一个字符串并缓存该值。
- 咱们应该减小没必要要的字符串操做。例如,若是咱们有一个常常更新的文本(Text)组件,而且包含一个链接的字符串,咱们能够考虑将它分红两个文本组件。
- 若是咱们必须在运行时构建字符串,咱们应该使用StringBuilder类。StringBuilder类用于构建没有分配的字符串,而且能够节省在链接复杂字符串时产生的垃圾数量。
- 咱们应该在调试再也不须要Debug.Log()调用时当即删除它们。对Debug.Log()的调用仍然在游戏的全部构建中执行,即便它们没有输出任何内容。对Debug. Log()的调用至少会建立并处理一个字符串,所以若是游戏包含许多此类调用,那么垃圾就会堆积起来。
让咱们看一个代码示例,它经过低效地使用字符串生成没必要要的垃圾。在下面得代码中,咱们经过将字符串“TIME:”与浮点计时器的值组合,在Update()中建立一个分数显示字符串。这会产生没必要要的垃圾。
public Text timerText;
private float timer;
void Update()
{
timer += Time.deltaTime;
timerText.text = "TIME:" + timer.ToString();
}
在下面的例子中,咱们对此进行了至关大的改进。咱们将单词“TIME:”放在一个单独的文本组件中,并在Start()中设置其值。这意味着在Update()中,咱们再也不须要组合字符串。这大大减小了生成的垃圾数量。
public Text timerHeaderText;
public Text timerValueText;
private float timer;
void Start()
{
timerHeaderText.text = "TIME:";
}
void Update()
{
timerValueText.text = timer.toString();
}
Unity的函数调用
最重要的是要意识到,不管何时咱们调用咱们尚未编写的代码,不管是在Unity自己仍是在插件中,咱们均可能在生成垃圾。一些Unity函数调用建立堆分配,所以应该当心使用,以免产生没必要要的垃圾。
没有咱们应该避免不去用的函数列表。每一个函数在有些状况下有用,而在其余状况下用处不大。和以往同样,最好仔细分析咱们的游戏,肯定垃圾在哪里被建立,并仔细考虑如何处理它。在某些状况下,缓存函数的结果多是明智的作法;在其余状况下,最好重构代码以使用不一样的函数。说了这么多,让咱们看几个会致使堆分配的Unity函数常见例子,并考虑如何最好地处理它们。
每当咱们访问一个返回数组的Unity函数时,就会建立一个新数组并将其做为返回值传递给咱们。这种行为并不老是明显的或可预期的,特别时当函数是一个访问器(例如,Mesh.normals)。
在下面的代码中,将为循环的每一个迭代建立一个新数组。
void ExampleFunction()
{
for (int i = 0; i < myMesh.normals.Length; i++)
{
Vector3 normal = myMesh.normals[i];
}
}
在这种状况下很容易既能够减小分配:咱们能够简单地缓存对数组的引用。当咱们这样作时,只建立了一个数组,并相应地减小了建立的垃圾数量。
下面代码说明了这一点。在这种状况下,咱们循环运行以前调用Mesh.normals,并缓存了引用,以便只建立一个数组。
void ExampleFunction()
{
Vector3[] meshNormals = myMesh.normals;
for (int i = 0; i < meshNormals.Length; i++)
{
Vector3 normal = meshNormals[i];
}
}
另外一个不可预期的堆分配能够在函数GameObject.name或者GameObject.tag中出现。这两个都是访问新字符串的访问器,这意味着调用这些函数将生成垃圾。缓存这个值可能颇有用,可是在这种状况下,咱们可使用一个相关的Unity函数去代替。为了在不产生垃圾的状况下检查GameObject的标签值,咱们可使用GameObject.CompareTag()。
在下面的示例代码中,调用GameObject.tag的时候建立垃圾。
private string playerTag = "Player";
void OnTriggerEnter(Collider other)
{
bool isPlayer = other.gameObject.tag == playerTag;
}
若是咱们使用GameObject.CompareTag(),这个函数将再也不产生任何垃圾:
private string playerTag = "Player";
void OnTriggerEnter(Collider other)
{
bool isPlayer = other.gameObject.CompareTag(playerTag);
}
GameObject.CompareTag不是惟一的例子:许多Unity函数调用都有不致使堆分配的替代版本。例如,咱们可使用Input.GetTouch()和Input.touchCount代替Input.touches,或者Physics.SphereCastNonAlloc()代替Physics.ShpereCastAll()。
装箱是当使用值类型变量代替引用类型变量时发生的操做。装箱一般发生在咱们将值类型变量(如int或float)传递给带有引用类型参数参数时。
例如,函数String.Format()接收一个字符串和一个对象参数。当咱们传递一个字符串和一个int时,这个int必须装箱。所以,下面的代码包含了装箱的一个例子:
void ExampleFunction()
{
int cost = 5;
string displayString = String.Format("Price: {0} gold", cost);
}
因为幕后发生的事情,装箱会产生垃圾。当一个值类型变量被装箱时,Unity会在堆上临时建立一个包装了值类型的System.Object类型的变量,所以当这个临时对象被丢弃时,就会建立垃圾。
装箱是形成没必要要堆分配的一个很是常见的缘由。即便咱们不直接在代码中对变量进行装箱,咱们也可能使用了致使装箱的插件,或者在其余函数的幕后进行装箱。最佳实践建议是尽量避免装箱,并删除致使装箱的任何函数调用。
协程(Cotoutines)
调用StartCoroutine()会建立少许垃圾,由于Unity必须建立实例来管理这个协程。考虑到这一点,在咱们的游戏是交互的而且性能要求须要考虑时,应该限制对StartCoroutine()的调用。为了减小以这种方式建立的垃圾,必须在性能关键时刻运行的任何协程都应该提早启动,在使用嵌套的协程时,咱们应该特别当心,由于它可能包含对StartCoroutine()的延迟调用。
协程中的yield语句自己不会建立堆分配:可是,使用yield语句传递的值可能会建立没必要要的堆分配。例如,下面的代码会建立垃圾:
这段代码建立了垃圾,由于值为0的int类型被装箱了。在这种状况下,若是咱们但愿简单地等待一个帧而不引发任何堆分配,那么最好的方法是使用如下代码:
协同程序的另外一个常见失误是在屡次使用相同值时使用new。例如,下面的代码将在每次循环迭代时建立并释放WaitForSeconds对象:
while (!isComplete)
{
yield return new WaitForSeconds(1f);
}
若是缓存和重用WaitForSeconds对象,则建立的垃圾会少不少。下面的代码说明改进的用法:
WaitForSeconds delay = new WaitForSeconds(1f);
while (!isComplete)
{
yield return delay;
}
若是咱们的代码因为协程而产生大量垃圾,咱们可能会考虑重构代码以使用协程以外的其余方法。重构代码是一个复杂的主题,每一个项目都是独特的,可是咱们可能但愿记住,协程有一个常见的替代方案。例如,若是咱们主要使用协程来管理时间,咱们可能但愿在Update()函数中简单地跟踪时间。若是咱们使用协程主要是为了控制游戏中事情发生的顺序,咱们可能但愿建立某种消息传递系统来容许对象进行通讯。对于这一点,没有一种适合全部人的方法,可是请记住,在代码中实现同一目标的方法一般不止一种。
foreach循环
在5.5以前的Unity版本中,foreach循环在每次结束时,遍历除数组以外的任何东西都会产生垃圾。这是因为装箱只在幕后进行的。一个System.Object在循环开始时分配到堆上,在循环结束时释放。这个问题在Unity 5.5中修复了。
例如,在5.5以前的Unity版本中,下面代码中的循环会产生垃圾:
void ExampleFunction(List listOfInts)
{
foreach (int currentInt in listOfInts)
{
DoSomething(currentInt);
}
}
若是咱们不能升级咱们的Unity版本,有一个简单的解决方案。for和while循环不会在幕后致使装箱,所以不会生成任何垃圾。在遍历非数组的集合时,咱们应该支持使用它们。
下面代码中的循环不会生成垃圾:
void ExampleFunction(List listOfInts)
{
for (int i = 0; i < listOfInts.Count; i ++)
{
int currentInt = listOfInts[i];
DoSomething(currentInt);
}
}
函数引用
对函数的引用,不管是引用匿名方法仍是命名方法,在Unity中都是引用类型变量。它们将致使堆分配。将匿名方法转换为闭包(在闭包中,匿名方法在建立时能够访问做用域中的变量)会显著增长内存使用和堆分配的数量。
函数引用和闭包如何分配内存的精确细节取决于平台和编译器的设置,可是若是垃圾回收是一个须要考虑的问题,那么最好在游戏过程当中尽可能减小函数引用和闭包的使用。这里
有一篇文章更详细地描述了有关这个这方面内容的技术细节。
LINQ和正则表达式
LINQ和正则表达式都会由于幕后的装箱而生成垃圾。最好避免在须要考虑性能的状况下使用它们。一样,
这里也有一篇关于这个主题的更多细节描述的文章。
构造代码以达到最小化垃圾回收的影响
咱们的代码的结构方式会影响垃圾回收。即便咱们的代码没有建立堆分配,它也会增长垃圾回收器的工做负担。
咱们的代码没必要要地增长垃圾回收器的工做负担的一种方式是要求它检查它不该该检查的东西。结构体是值类型变量,可是若是咱们有一个包含引用类型的结构体,那么垃圾回收器必须检查整个结构体。若是咱们有大量这样的结构体,那么这会为垃圾回收器增长大量额外的工做。
在这个例子中,结构体包含一个引用类型的字符串。垃圾回收器在运行时必须检查整个结构体数组。
public struct ItemData
{
public string name;
public int cost;
public Vector3 position;
}
private ItemData[] itemData;
在下面这个例子中,咱们将数据存储在不一样的数组中。当垃圾回收器运行时,它只须要检查字符串数组,并能够忽略其余数组。这减小了垃圾回收器必需要作的工做。
private string[] itemNames;
private int[] itemCosts;
private Vector3[] itemPositions;
代码没必要要地在增长垃圾回收器工做负担的另外一种方式是使用没必要要的对象引用。当垃圾回收器搜索堆上对象的引用时,它必须检查代码中的每一个当前对象的引用。代码中对象引用的减小意味着它要作的工做更少,即便咱们不减小堆上对象的总数。
在这个例子中,咱们有一个弹出对话框的类。当用户查看该对话框时,将显示另外一个对话框。咱们的代码包含对下一个应该显示的DialogData实例的引用,这意味着垃圾回收器必须将该引用做为其操做的一部分进行检查。
public class DialogData
{
private DialogData nextDialog;
public DialogData GetNextDialog()
{
return nextDialog;
}
}
在这里,咱们从新构造了代码,使其返回一个标识符,用于查找DialogData的下一个实例,而不是实例自己。这不是一个对象引用,所以它不会增长垃圾回收器所花费的时间。
public class DialogData
{
private int nextDialogID;
public int GetNextDialogID()
{
return nextDialogID;
}
}
就其自己而言,这个示例至关简单。可是,若是咱们的游戏包含大量对象,这些对象包含对其余对象的引用,那么咱们能够经过以这种方式重构代码来大大下降堆的复杂性。
手动强制垃圾回收
最后,咱们可能但愿本身触发垃圾回收。若是咱们知道堆内存被分配,但再也不使用(例如,假如我i们的代码在加载资源的时候生成垃圾),而且咱们知道垃圾回收冻结也不会影响玩家(例如,加载屏幕仍然显示),咱们可使用下面的代码请求垃圾回收:
这将强制垃圾回收器运行,在咱们方便的时候释放未使用的内存。
总结
咱们已经学习了Unity中的垃圾回收是如何工做的,为何垃圾回收会致使性能问题,以及如何最小化垃圾回收对游戏的影响。利用这些知识和咱们的分析工具,咱们能够修复与垃圾回收相关的性能问题,并构建游戏的结构,从而有效地管理内存。
下面的连接提供了关于本文主题的进一步阐述。
延伸阅读
Unity中的内存管理和垃圾回收