引言
游戏运行时使用内存来存储数据,当这些数据不再被使用时,存储这些数据的内存被释放以便于之后这些内存可以被复用。垃圾(Garbage )是存储无用数据的内存的术语,GC(Garbage Collection 垃圾回收)是使这些内存可以再次使用的过程。
GC是Unity管理内存的一部分,我们的游戏可能因为GC负担过重而表现不佳,所以GC是引起性能问题的一个常见原因。
在这篇文章中,我们将介绍GC如何工作,在什么情况下会触发GC和如何高效的使用内存以减少GC对游戏的影响
GC问题诊断
GC引起的性能问题可表现为帧率过低,帧率剧烈波动或者间歇性卡顿。但是其他问题也可能引起类似的症状。如果你的游戏有这些性能问题,首先需要使用Unity的Profiler工具来确定这些问题是由GC引起的。
如何使用Profiler工具来确定引起性能问题的原因,可以查看 这篇教程。
Unity内存管理简介
在了解GC如何工作和何时触发之前,我们需要先了解Unity的内存使用情况。首先,我们要知道,在运行自己的核心引擎代码和运行我们在脚本中编写的代码时,Unity使用不同的方法。
当Unity在运行自己的核心引擎代码时使用 手动内存管理,这意味着核心引擎代码必须明确地说明如何使用内存。手动内存管理不使用GC,本文不做介绍。
当Unity运行我们写的脚本代码时使用 自动内存管理,这意味着我们写代码时不用明确的告诉Unity如何管理内存,Unity自动帮我们完成这些工作。
基本上来说,Unity自动内存管理像这样工作:
- Unity可以访问两个内存池:栈和堆(也称为托管堆)。栈用于短期存储小块数据,堆用于长期存储和较大数据段。
- 当创建变量时,Unity从栈或堆中申请内存
- 只要变量在作用域内(仍然可以通过我们的代码访问),分配给它的内存仍然在使用中, 我们称这部分内存已被分配。 我们将栈中的变量称为栈对象,将堆中的变量称为堆对象。
- 当变量超出作用域,该内存不再被使用并可以归还给原来的内存池。当内存被归还给原有的内存池里,我们称该内存被释放。栈内存在变量超出作用域时被实时释放,而堆内存在变量超出作用域之后并没有被释放并保持被分配的状态
- 垃圾收集器(garbage collector)识别和释放未使用的堆内存。 垃圾收集器定期运行以清理堆。
现在我们了解事件的流程,让我们进一步了解栈分配和释放与堆分配和释放之间的区别。
在栈分配和释放时发生了什么
栈分配和释放简单快速。这是因为栈只用于在短时间内存储小数据。 分配和释放总是以可预测的顺序发生,并且具有可预测的大小。
栈的工作方式类似于 栈数据类型: 它是一个简单的元素集合,这种情况下的内存块,只能以严格的顺序添加和删除元素。 这种简单性和严格性使得它变得非常快速:当一个变量存储在栈上时,它的内存就是 简单地 从栈顶分配。 栈变量超出作用域时,用于存储该变量的内存将立即返回栈进行重用。
在堆分配时发生了什么
堆分配比栈分配复杂的多。因为堆可以用来存储长期和短期数据及各种不同类型大小的数据。分配和释放也并不总是按可预测的顺序进行且可能需要大小差距巨大的内存块。
当一个堆变量创建时,将执行以下步骤:
- 首先,Unity检查堆上是否有足够的空闲内存,如果有,则该变量的内存被分配。
- 如果没有,Unity触发GC试图释放未使用的堆内存,这个操作可能很慢。如果GC之后堆内存足够,则该变量的内存被分配。
- 如果GC之后堆上还是没有足够的空闲内存,Unity将向操作系统申请更多内存以扩大堆大小。这个操作可能很慢。之后该变量的内存被分配。
堆分配可能会很慢,特别在必须执行GC和扩大堆大小时。
在GC时发生了什么
当堆变量超出作用域后,存储该变量的内存并没有被立即释放。无用的堆内存只在执行GC时被释放。
每次执行GC时, 将执行以下步骤:
- 垃圾收集器检索堆上的每个对象。
- 垃圾收集器搜索所有当前对象引用以确定堆上的对象是否仍在作用域内。
- 不在作用域内的对象被标记为删除。
- 删除被标记的对象并将内存返回给堆。
GC是个费时的操作,堆上的对象越多,代码中的引用数越多,GC就越费时。
何时会触发GC
三种情况下会触发GC:
- 堆分配时堆上的可用内存不足时触发GC。
- GC会不时的自动运行(频率因平台而异)。
- 手动强制调用GC
GC可能被频繁触发。每当无法从可用堆内存中实现堆分配时,就会触发GC,这意味着频繁的堆分配和释放可能导致GC频繁。
GC的问题
现在我们了解了GC在Unity内存管理中的作用,我们可以考虑可能发生的问题类型。
最明显的问题是GC可能花费相当长的时间来运行。 如果堆上有很多对象和大量的对象引用要检查,则检查所有这些对象的过程可能很慢。 这可能会导致我们的游戏卡顿或运行缓慢。
另一个问题是GC可能在不合时宜的时刻被触发。