本篇文章是结合各个大佬的文章而成
1.unity内存管理简介:
内存的分配应该都是以C#作为标准,但是GC是不通的框架有不同的标准(用了不通的GC算法)。
(Mark-Compact)标记压缩法
1)unity内部有两个内存管理池:堆内存和堆栈内存。堆栈内存(stack)主要用来存储较小的和短暂的数据,堆内存(heap)主要用来存储较大的和存储时间较长的数据。
2)unity中的变量只会在堆栈或者堆内存上进行内存分配,变量要么存储在堆栈内存上,要么处于堆内存上。
3)只要变量处于激活状态,则其占用的内存会被标记为使用状态,则该部分的内存处于被分配的状态。
4)一旦变量不再激活,则其所占用的内存不再需要,该部分内存可以被回收到内存池中被再次使用,这样的操作就是内存回收。处于堆栈上的内存回收及其快速,处于堆上的内存并不是及时回收的,此时其对应的内存依然会被标记为使用状态。
5) 垃圾回收主要是指堆上的内存分配和回收,unity中会定时对堆内存进行GC操作。
此处插入.NET GC的过程
1.root入口
.NET中可以当作GC Root的对象有如下几种:
1、全局变量
2、静态变量
3、栈上的所有局部变量(JIT)
4、栈上传入的参数变量
5、寄存器中的变量
2.有了入口就是各种清理方法:
1.标记清除法:
原理:从GC Root开始递归,对可能引用的对象进行标记,没有标记的作为垃圾被回收
步骤:遍历并标记对象->回收死亡对象,清除存活对象的标记
缺点:
1.清除阶段还需要对大量死亡对象进行扫描,死亡对象多的话会相当耗时
2.清理出来的内存空间不连续
2.标记整理法:(上一种方法+内存压缩)
**原理:**从GC Root开始递归,对可能引用的对象进行标记,之后移动所有存活的对象,且按照内存地址次序依次排列,然后将末端内存地址以后的内存全部回收
**步骤:**遍历并标记对象->整理存活对象->回收
**缺点:**效率低
3.复制清除法:
**原理:**遍历GC Root引用的对象,复制到另外的空间,并递归地对复制对象引用的对象进行复制,之后清除旧空间
**步骤:**递归复制->废弃旧空间
缺点:
1.复制开销大,存活对象多耗时大
2.浪费一半的内存
4.引用计数法:
**原理:**为每个对象保存引用计数,引用增减时更新计数
步骤:不需要扫描,对计数0的对象进行垃圾回收
缺点:
1.无法释放循环引用(互相引用)的对象
2.引用计数不能遗漏
3.不适合并行处理
5.分代搜集法:
原理:对分配时间短的对象进行清理
比如Net,将内存中的对象分为了三代,每执行N次0代的回收,才会执行一次1代的回收,而每执行N次1代的回收,才会执行一次2代的回收。当某个对象实例在GC执行时被发现仍然在被使用,它将被移动到下一个代中上。(回收频率是100:10:1)
而在mono 中是分2代
各平台GC算法
关于Mono ,集成的是开源项目BOEHM ,BOEHM算法采用标记清除法
Mono的GC之旅
Stack内存分配和回收机制:
其实就是普通栈的方式,有效数据压入栈,无效后移除。
Heap内存分配和回收机制:
这里就需要知道托管堆的概念!Heap其实就是托管堆了,Heap存储引用类型的数据,例如动态分配的数组其实是在托管堆上的,由GC管理这部分内存。
Heap内存分配步骤:
1)首先,unity检测是否有足够的闲置内存单元用来存储数据,如果有,则分配对应大小的内存单元;
2)如果没有足够的存储单元,unity会触发垃圾回收来释放不再被使用的堆内存。这步操作是一步缓慢的操作,如果垃圾回收后有足够大小的内存单元,则进行内存分配。
3)如果垃圾回收后并没有足够的内存单元,则unity会扩展堆内存的大小,这步操作会很缓慢,然后分配对应大小的内存单元给变量。
堆内存的分配有可能会变得十分缓慢,特别是在需要垃圾回收和堆内存需要扩展的情况下,通常需要减少这样的操作次数。
Heap垃圾回收步骤:
**类似标记清除法 ** 不会进行压缩 所以内存碎片一直存在
1)GC会检查堆内存上的每个存储变量;
2)对每个变量会检测其引用是否处于激活状态;
3)如果变量的引用不再处于激活状态,则会被标记为可回收;
4)被标记的变量会被移除,其所占有的内存会被回收到堆内存上。
何时触发GC:
1) 在堆内存上进行内存分配操作而内存不够的时候都会触发垃圾回收来利用闲置的内存;
2) GC会自动的触发,不同平台运行频率不一样;
3) GC可以被强制执行。
GC的问题:
耗时、耗能、影响游戏运行的流畅度(帧率)、运行缓慢、产生大量内存碎片(因为没有内存压缩)占用内存会越来越大!
如何判断游戏卡顿是由于GC造成:
打开unity中的profiler window来确定是否是GC造成
如果游戏卡顿是由于GC造成的:
分析堆内存的分配
降低GC的影响的方法
大体上来说,我们可以通过三种方法来降低GC的影响:
1)减少GC的运行次数;
2)减少单次GC的运行时间;
3)将GC的运行时间延迟,避免在关键时候触发,比如可以在场景加载的时候调用GC
似乎看起来很简单,基于此,我们可以采用三种策略:
1)对游戏进行重构,减少堆内存的分配和引用的分配。更少的变量和引用会减少GC操作中的检测个数从而提高GC的运行效率。
2)降低堆内存分配和回收的频率,尤其是在关键时刻。也就是说更少的事件触发GC操作,同时也降低堆内存的碎片化。
3)我们可以试着测量GC和堆内存扩展的时间,使其按照可预测的顺序执行。当然这样操作的难度极大,但是这会大大降低GC的影响。
减少内存垃圾的数量
不要在频繁调用的函数中反复进行堆内存分配
对象池
清除链表
造成不必要的堆内存分配的因素
字符串
1.减少Debug.Log()
2.减少字符串创建
3.如果有类似”Time:###“,Time不变,#变的情况,可以使用两个Text组件。
4.实时创建字符串时,可以使用stringbuilder。
函数调用
1.降低调用频率。
避免装箱
协程
调用 StartCoroutine()会产生少量的内存垃圾,因为unity会生成实体来管理协程。所以在游戏的关键时刻应该限制该函数的调用。基于此,任何在游戏关键时刻调用的协程都需要特别的注意,特别是包含延迟回调的协程。
yield在协程中不会产生堆内存分配,但是如果yield带有参数返回,则会造成不必要的内存垃圾,例如:
yield return 0;
由于需要返回0,引发了装箱操作,所以会产生内存垃圾。这种情况下,为了避免内存垃圾,我们可以这样返回:
yield return null;
另外一种对协程的错误使用是每次返回的时候都new同一个变量,例如:
while(!isComplete)
{
yield return new WaitForSeconds(1f);
}
我们可以采用缓存来避免这样的内存垃圾产生:
WaitForSeconds delay = new WaiForSeconds(1f);
while(!isComplete)
{
yield return delay;
}
如果游戏中的协程产生了内存垃圾,我们可以考虑用其他的方式来替代协程。重构代码对于游戏而言十分复杂,但是对于协程而言我们也可以注意一些常见的操作,比如如果用协程来管理时间,最好在update函数中保持对时间的记录。如果用协程来控制游戏中事件的发生顺序,最好对于不同事件之间有一定的信息通信的方式。对于协程而言没有适合各种情况的方法,只有根据具体的代码来选择最好的解决办法。
**foreach 循环 ** 5.5版本以下 会产生装箱操作
函数引用
**重构代码来减小GC的影响 ** 例如将结构体拆分
**定时执行GC操作 ** 在游戏非关键时期进行主动GC