一、内存种类
实际上Unity游戏使用的内存一共有三种:程序代码、Managed Memory(分为堆栈和托管堆)以及Native 内存(Native Heap,或者叫Native堆,原生内存)。
二、程序代码内存优化
减少一些非要的包含库,没有用到的插件。
三、Managed Memory
1、定义。是mono虚拟机VM的内存,主要由VM的GC机制管理。这部分内存属于语言级别的。其内存占用量一般较小,主要目的是程序猿在处理程序逻辑时使用。通常如通过System命名空间中的接口分配的内存,通过Mono Runtime分配在Mono堆上。
2、管理机制。unity主要用Mono的GC机制管理这部分内存。
3、GC的Block 机制。内存也是以 Block 来管理的。当一个 Block 连续六次 GC 没有被访问到,这块内存才会被返还到系统。(mono runtime 基本看不到,IL2cpp runtime 可能会看到多一点)
-
4、内存类型上划分。
- a、堆栈储存值类型。在运行时C#会为每个线程分配堆栈,之后从入口函数的执行开始,所用到的值类型、函数指针、引用等顺序入栈,当一个函数执行完毕后,则其使用的局部变量出栈直到回到函数指针所在位置。堆栈是系统管理的。
- b、托管堆,储存引用类型。当我们显式分配内存的时候就会分配在托管堆上,例如new一个对象。托管堆上还保存了每个类型的类型对象(继承自Type)。
-
5、有哪些资源或调用会产生托管堆内存?
- a、创建游戏对象:通过使用新的GameObject或Instantiate来创建新的游戏对象会导致堆内存的分配。尽管GameObject本身的内存占用可能并不大,但是如果它挂载了很多的组件或脚本,那么可能会占用更多的内存。
- b、创建数组或集合:数组和集合都会分配堆内存。这包括ArrayList、List、Dictionary等类型。
- c、字符串操作:在C#中,字符串是不可变的。每次对字符串进行修改,比如使用"+"或"Substring"方法,都会创建一个新的字符串,会占用更多内存。
- d、使用lambda表达式:使用匿名函数或lambda表达式可能会在堆上分配内存。这是因为编译器为匿名函数创建了一些闭包数据结构,并在堆上分配这些结构。
- e、创建类的实例:每次使用new关键字创建类的实例,都会在堆上分配内存。
- f、使用Boxing:Boxing是将值类型(例如,整数,浮点数,结构等)作为引用类型(例如,object或特定的接口)处理的过程。Boxing会在堆上分配额外内存。
6、托管堆内存释放的时机
1)当你使用完一个实例对象之后,通常来说在脚本中就不会再有对该对象的引用了(这包括将变量设置为null或其他引用,超出了变量的作用域,或者对Unity对象发送Destory())。Block机制,在每隔一段时间,Mono的垃圾回收机制将检测内存,将没有再被引用的内存释放回收。
2)在切换场景的时候,托管堆资源都会释放出来。7、释放注意点
1)不是越多调用GC就越好。在内存清理时有可能造成游戏的短时间卡顿,这将会很影响游戏体验,因此如果有大量的内存回收工作要进行的话,需要尽量选择合适的时间。如果在你的游戏里,有特别多的类似实例,并需要对它们经常发送Destroy()的话,游戏性能上会相当难看。这时候可以考虑使用对象池机制来应对这种情况。
2)避免装箱操作。
3)String字符串拼接代替直接的+号
4)闭包分配。匿名函数或Lamda表达式时有时会产生。
5)避免使用Linq库。8、Singleton也要慎用。游戏一开始到游戏死掉,一直在内存中。
四、Native 内存
- 1)主要是C++管理的,unity本身是一个C++引擎,这部分内存是属于应用级别的,这部分内存的释放需要使用unity引擎自身提供的方法才能进行释放。
- 2)Unity 重载了所有分配内存的操作符(C++ alloc、new),这部分本身是c++的,当我们 delete、free 一个内存的时候会立刻返回内存给系统,与托管内存堆(Managed 内存)不一样。
- 3)卸载办法。常见有Destory方法,Resource.UnloadAsset()和Resources.UnloadUnusedAssets(),这些引擎的管理内存方法,需要根据创建内存的类型来对应使用。
- 4)优化方式。除了使用完了之后及时清理的优化方式之外,每一种内存类型,在产生或制造的时候就要注意其大小和数量。比如Texture,Mip Maps,作为UI 没必要开,可以省大量内存。r/w同理没必要就不开。音频的压缩,ab包的压缩方式等。
有哪些资源或调用会产生本机堆内存(Native内存)?
- 1)资源:如Texture、Mesh和Audio等资源,以及通过Asset Bundle载入的资源对象,都会在Native Memory中占用内存。
- 2)游戏对象(GameObjects)和组件(Components):每当一个GameObject或Component被创建时,它的实例数据就存储在Native Memory中。
- 3)物理模拟:物理引擎的运算结果、碰撞体、刚体等都占用Native Memory。
- 4)导航网格和寻路数据:如果你使用的是Unity的NavMesh功能,那么导航网格也存储在Native Memory中。
- 5)渲染数据:包括渲染缓冲区,渲染贴图,等等。
- 6)Unity内部使用的一些其他数据结构:如场景图等等。
注意点:
- 1)大部分脚本将在场景转换时随之失效并被回收,但是,在场景之间被保持的脚本不在此列(通常情况是被附 着在DontDestroyOnLoad的GameObject上了)。而这些脚本很可能含有对其他物体的Component或者资源的引用,这样相关的 资源就都得不到释放。
- 2)static的单例(singleton)在场景切换时也不会被摧毁,同样地,如果这种单例含有大量的对资源的引用,也会成为大问题。
- 3)Unity在一个场景开始时,根据场景构成和引用关系所自动读取的资源,会直接读取到内存中。这种内存无法用Resource.UnloadAsset()和Resources.UnloadUnusedAssets()去管理,因此尽量不要在场景中有过多的直接资源引用。
这篇文章主要强调手动去释放unity的堆内存,下一篇中将会介绍利用Unity内存管理GC机制来协助管理好托管堆内存。