游戏运行时使用内存来存储数据,当这些数据不再被使用时,存储这些数据的内存被释放以便于之后这些内存可以被复用。
一 核心概念
垃圾:存储这些无用数据的内存的术语。
GC(Garbage Collection):使垃圾(无用内存)可以再次使用的过程。
需要优化GC的原因:
①因为GC的触发是有条件触发,当垃圾存储到一定时候,会被动触发回收这些无用内存。如果垃圾内过多,被动触发次数过多,
会造成CPU负荷过多,GC是由CPU启动的。
②每次触发GC,会有明显的卡顿,帧率降低,尤其低端手机。如果游戏在战斗时候发生卡顿,就是很不好的体验了。
二 Unity内存管理
被释放:当变量超出作用域时,该内存不再被使用并可以归还给原来的内存池,当内存被归还给原有的内存池里。
被分配:变量在作用域内,分配给他的内存仍然在使用中,我们称这部分内存已被分配。
栈:Unity可以访问的内存池之一,用于短期存储小块数据,变量超出作用域时被自动实时释放。
堆:Unity可以访问的内存池之一,用于长期存储和较大数据块。变量超出作用域时并没有被释放,还是继续保持被分配状态。
垃圾收集器(garbage collector):识别和释放未使用的堆内存。垃圾收集器定期清理堆。
栈分配和释放过程:栈分配和释放简单快速。这是因为栈只用于在短时间内存储小数据。分配和释放总是以可预测的顺序发生,
并具有可预测的大小。栈的工作方式类似于栈数据类型:这是一个简单的元素集合,这种情况下的内存块,只能以严格的顺序添加和删除元素。这种简单性和
严格性使得它变得非常快速。
堆分配过程:堆分配比栈分配复杂额多。因为堆可以用来存储长期和短期数据及各种不同类型大小的数据。分配和释放并不总是按可预测的顺序
进行且可能需要大小差距巨大的内存块。
当一个堆内存创建时,将执行以下步骤:
①Unity检查堆上是否有足够的空间内存,如果有,为该变量被分配内存。如果没有,Unity触发GC试图释放未使用的堆内存,这个操作可能很慢。
如果GC之后堆内存足够,则该变量被分配内存 。
②GC之后堆上还是没有足够的空闲内存,Unity将向操作系统申请更多内存以扩大堆大小。这个操作可能很慢。堆分配可能会很慢,特别在必须执行GC和扩大堆大小时。
(GC是个费时的操作,堆上的对象越多,代码中的引用数越多,GC越费时)
GC时的具体的步骤:
①垃圾收集器检索堆上的每个对象、
②垃圾收集器搜索所有当前对象引用以确定堆上的对象是否仍在作用域内
③不在作用域内的对象呗标记为删除
④删除被标记的对象并将内存返回给堆。
何时触发GC:
堆分配时堆上的可用内存不足时触发GC。
GC会自动运行。(频率因平台而异)
手动强制调用GC。
三 GC 的问题
如果堆上有很多对象和大量的对象引用要检查,则检查所有这些对象的过程可能很慢。这可能导致游戏卡顿或缓慢运行。
GC在不合时宜的场合被触发。如果CPU在我们游戏的性能关键部分已经满负荷了,那此时即使是少量的GC额外开销也可能导致
我们的游戏卡顿或运行缓慢。
堆碎片,当从堆中分配内存时,会根据必须存储的数据大小从不同大小的块中的可用空间中获取内存。
当这些内存返回到堆时,堆可能分成很多由分配块分隔的小空闲块.这意味着虽然可用内存总量很高。但是由于
碎片化严重而无法分配一块连续的大内存。这意味着GC被触发或不得不扩大堆大小。
(严重后果:①游戏内存大小会高于实际所需要的大小 ②GC会被频繁的触发)
四 优化GC(减少GC的次数)
①尝试在合适时机(loading时),手动触发GC和扩展堆大小以便GC可控。
②缓存,将局部函数中的局部引用变量写成公共。
③对象池。
④清理容器。
⑤字符串的创建之类。
⑥Debug.Log的引用。
⑦注意装箱,装箱会产生垃圾源于底层,当一个值类型变量被装箱时,Unity在堆上创建一个临时的System.Object
来包装值类型变量。一个System.Object是一个引用类型的变量,所以当这个临时对象被处理时会产生垃圾。
⑧StartCortine会产生少量垃圾。
yield return 0;//会产生垃圾,int变量0被装箱。
=>>> yield return null
yield return new WairforSeconds(1f);//如果多次被调用,会产生很多。
==>>>WaitForSeconds delay =new WaitForSeconds(1f);//可以事先缓存起来
yield return delay;
⑨Linq和正则表达式在后台有装箱操作而产生垃圾,最好少使用。
⑩构建代码以最小化GC的影响
代码的构建方式可能会影响GC,即使代码中没有堆分配,也可能会增加GC的负担。可能增加GC的负担之一
是要求检查他不该检查的东西。Struct是值类型变量,但是如果包含一个引用类型便利的struct,那么垃圾收集器必须检查整个
结构体。
另外一个增加GC负担的操作是使用不必要的对象引用,当垃圾收集器搜索堆上对象的引用时,它必须检查代码中的
每个当前对象的引用。更少的对象引用意味着更少的工作量。