1、Unity内存按照分配方式分为:
(1)Native Memory(本地内存)
(2)Managed Memory(托管内存)
Editor和Runtime内存管理是不同的,Editor下打开工程就会将所有资源加载进内存,而Runtime是我们调用相应的Load接口时才会加载进内存。
2、Unity按照管理者分为:
(1)引擎管理内存
(2)用户管理内存
3、Unity Native Memory管理
图3-1 Native-Memory分配流程Unity通过重载C++的new/malloc内存分配操作符实现自己的内存管理。
3.1 Allocator与memory label
memory label区分内存类型,每一种内存类型都会对应一个Allocator,每个Allocator有各自的分配策略,每一个Allocator会跟踪一个内存池。当需要申请内存时,就会找到对应类型的Allocator进行分配。
3.2 NewAsRoot
调用NewAsRoot时会分配所有
3.3 Stack Allocator
Stack Allocator用于分配局部内存,Stack Allocator的特点就是快、小、临时。
当Stack Allocator分配内存时,一次分配一个大的Heap Block块,然后通过栈的方式管理这块Block的分配。实际分配内存时会包含一个Header和User Data,Header记录当前使用状态、User Data大小等信息。使用一个栈顶指针管理内存分配,每次分配只在栈顶顶端新分配一块内存,然后修改栈顶指针。回收时如果回收的是非栈顶内存只需要修改Header中使用状态为Deleted;如果回收的是栈顶指针指向的内存,修改标记状态同时,栈顶指针向上回弹,回弹后会查看当前指向的内存是否已标记为Deleted,如果是则继续回弹,直到栈顶指针指向一个未标记为Deleted的内存。
图3-2 Stack-Allocator示意图当一个Block块无法分配后,会再申请一块Block。但不可能无限分配,Stack Allocator有大小限制;
Editor:16MB Main Thread,256KB workers;Runtime:1MB Main thread,64KB workers
如果我们需求内存过大导致Stack Allocator大小不够,Unity会执行Fallback机制,会使用Dynamic Heap Allocator分配内存。Dynamic Heap Allocator分配策略相对Stack Allocator复杂很多,所以耗时比较大,出现这种情况可能导致卡顿。出现这种情况时Unity会给出一个MemoryManager.FallbackAllocation的提示。所以可以分帧加载一个比较大或多的资源。
3.4 影响Native内存大小因素
(1)Scene:场景中GameObject越多占用内存越大
(2)Audio
a、DSP Buffer Size(PlayerSettings->Audio)
声音数据缓冲,太大可能导致声音延时,太小导致发送给CPU太频繁
b、Force to mono:强制改成单声道,内存相对双声道减半
c、Format
d、Compression Format
(3)Code Size
模板泛型滥用
(4)AssetBundle
a、TypeTree:保证不同版本序列化正确
如果确保Bundle版本和Unity版本一致,打Bundle时可以通过设置BuildAssetBundleOptions.DisableWriteTypeTree关闭TypeTree,减小包体、省内存、并且可以提升打包速度。
b、LZ4(trunk based)/LZMA
c、设置合适的AB大小和数量
(5)Resources文件夹
打包时会生成R-B Tree用于Resources检索
(6)Texture
a、upload buffer(Project Settings->Quality->Async Upload Buffer Size)
b、read/write
c、Mip Maps
(5)Mesh
a、read/write
b、compression
(6)Assets
4、Unity Managed Memory
图4-1 Managed Memory分配机制4.1
Unity使用Boehm回收器,Boehm是保守式回收器。
由于分代式垃圾回收移动内存、压缩等开销较大,不适合移动平台,所以Unity选择使用Boehm回收。
Unity不论使用Mono还是IL2CPP都使用Boehm回收器,
Unity嵌入早期版本的Mono(具体原理可参考Mono自动内存管理分析),采用非分代、非压缩的保守式GC,托管内存只增不减。
IL2CPP由Unity自己重写垃圾回收机制,是升级版的Boehm。使用IL2CPP,托管内存可以降低,内存返还的条件就是当GC6次,某一个Block都是闲置时,就会将这个Block返还给系统。
图4-2 回收机制展示4.2 Managed Memory最佳实践
(1)使用Destroy,别用Null;
(2)Class VS Struct;
(3)缓存池;
(4)Closures and anonymous methods(闭包和匿名函数);
(5)Coroutines(协程);
(6)Configuration(配置表);
(7)Singleton
(8)先分配大内存再分配小内存减少碎片化
4.3 Incremental GC
Unity2019.1引入Incremental GC(渐进式GC),在使用Boehm回收器的基础上以增量模式运行,将垃圾收集拆分到多帧进行。
大多数情况下,Incremental GC可以减少垃圾收集尖峰问题。但某些情况下,使用Incremental GC可能会产生其它问题:
(1)Incremental GC是拆分的标记阶段,当对象引用改变时,必须在下一次迭代中再次扫描这些对象,如果在标记阶段不断有引用关系的变化,可能导致标记遍历永远不能完成,这种情况下,垃圾收集会退回到进行完整的非增量收集。
(2)另外,Incremental GC时,引用发生改变,Unity就需要设置写屏障通知垃圾收集,会增加开销,对代码产生性能影响。
5、Native和Managed关系
图5-1 Native和Manager Object关系(视频地址:https://www.bilibili.com/video/BV1J64y187TW?p=3)如图5-1,上半部分是Native Object创建过程,下半部分是Managed Object创建过程。实际上不论我们是创建一个native object还是managed object都可能同时创建一个关联的managed/native object。
比如加载一个Texture会创建一个native object实例,同时也会创建一个托管的Texture实例。
Texture _texture = Resources.Load<Texture>("Unity");
我们将_texture置为null,然后调用GC.Collect();此时在Profiler中查看_texture实例并没有被回收,而当我们调用Resources.UnloadUnusedAssets()后_texture销毁了。
网友评论