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