浅谈Unity内存管理
这是根据2019年官方分享所做的笔记【[Unity 活动]-浅谈Unity内存管理】
Linux的内存占用指标

- RSS :自己使用的内存+调用公共服务使用的全部内存
- PSS : 自己使用的内存+所有调用公共服务的app平均后的内存
- USS : 自己使用的内存
内存分类
按照分配方式分
- Native Memory 本地内存:本地内存,指那些不被托管的内存,需要手动申请与释放
- Managed Memory 托管内存: 指受GC托管的内存
- Tips : Unity 中 Editor 模式与 Run Time 模式内存统计与分配是不一样的。
按照管理者分
- Unity管理
- 用户管理
Unity无法监测到的内存
- 用户的Native Memory
- Lua也是用户自管理的内存
Unity Native Memory 最佳实践
Scene
构建Gameobject时底层会使用C++构建多个Object来存储信息,当检查到Unity Native Memory大量上升时,检查场景中是否存在大量物体
Audio
- DSP Buffer 声音缓存,当满了才会向CPU发送一次请求,设置过大容易导致声音延迟,过小容易造成多次CPU请求导致消耗增大
- Force to mono 强制单声道,对于声音不敏感的场合可以大量减少内存使用
- Format 格式要求,各平台要求不一
- Compression 压缩方式
- Code Size 代码所占用的内存,这里模板函数与泛型,同时也会影响打包函数,这里应该深入了解C++的底层编译泛型
Asset Bundle
Type Tree
细致的这里挖个坑,等我后续学习完AssetBundle后回来补充
Unity 的每一种类型都有很多数据结构的改变,为了对此做兼容,Unity 会在生成数据类型序列化的时候,顺便会生成 TypeTree:当前我这一个版本里用到了哪些变量,对应的数据类型是什么。在反序列化的时候,会根据 TypeTree 来进行反序列化。
- 如果上一个版本的类型在这个版本中没有,TypeTree 就没有它,因此不会碰到它。
- 如果要用一个新的类型,但在这个版本中不存在,会用一个默认值来序列化,从而保证了不会在不同的版本序列化中出错,这就是 Type Tree 的作用。
关于关闭Type Tree:当版本与Unity一致时可关闭,可以减小内存
关闭的好处:
- 内存减少,本身Type Tree就会占用一定内存
- 包大小减少,type tree 会被序列化到包内
- build 和 runtime会变快,因为序列化会先type tree序列化再将实际的东西序列化,反序列化也同理。
两种压缩方式
- Lz4 建议使用,速度更快虽然占用会多30%左右,并且后续有加密支持
- Lzma
Size&Count
分包大小建议1M-2M,现在可以适当增大。如果每个Asset都打一个包,会导致最终包头比实际数据还大。
Resources 文件夹
现在不推荐使用,建议使用Asset Bundle。
打包时会生成一颗红黑树,在游戏启动时会一直存在内存中,不可卸载。
Texture
- upload buffer 与上述音频的DSP Buffer基本一样。
- r/w 可读写性,若开启,当材质载入buffer后,内存中的那份不会被删除,导致显存与内存同时存在两份
- mesh -r/w -compression 有些版本可能开了反而更占用内存。
Asset
Unity Managed Memory
即unity 托管内存部分
- VM :内存池 mono的内存池 VM的内存会返还给操作系统,但是条件苛刻:连续6次GC没有访问到,才会返还。mono runtime基本不可能,L2cpp runtime可能才会有一点。
- GC 机制: GC不会返还内存给系统
GC评价指标以及分类:
- Throughput 回收能力:一次回收可以回收多少内存
- Pause Times 暂停时长:回收时对主线程的影响
- Fragmentation 碎片化:回收内存后,会对整体回收内存池的贡献有多大
- Mutator Overhead 额外消耗:回收本身有 overhead,要做很多统计、标记的工作
- Scalability 可扩展性:多核多线程会有问题吗
- Portability 可移植性:其他平台
现在使用的GC是boehm 非分代式GC、非压缩式 GC(26年注:现在也已经淘汰)
- 分代式:指大内存、小内存、超小内存分布在不同内存区域管理,甚至还有长久内存
- 压缩式:当一个内存回收后,会重新排布再将各部分使用的内存紧密排布
仍然使用原因:一部分历史原因是与Mono合作的问题,另一原因是目前转向下一代GC:Incremental GC
Incremental GC 解决问题(26年注:现在使用的GC)
- 进行一次 GC,主线程被迫要停下来,遍历所有 GC Memory elements,来决定哪些可以被 GC 回收。
- Incremental GC 把暂停主线程的事分帧做了。一点一点分析,主线程不会有峰值。总体 GC 时间不变,但会改善 GC 对主线程的卡顿影响。
Zombie Memory 僵尸内存
由于内存过量碎片化,导致加载一些大内存时无法放入,这并不是内存泄露,内存泄露指的是一块内存无法被管理或访问。
几个优化建议:
- Don't Null it, but Destroy it. 销毁而不是赋值为空
- Class vs Struct Class是引用类型而Struct是值类型
- Pool In Pool 建议对高频使用小部件自建对象池,比如UI 粒子系统 子弹等等
- Closures and anonymous metheds 闭包和匿名函数
- Coroutines 协程,这是闭包和匿名函数的一个特例 这里挖个坑后续多研究
- Configurations 配置表 Singleton 单例 慎用,看里面放的变量,这些从始至终都会一直占用内存
定量调试工具:UPR
后续学习后补充......
许可协议:
CC BY 4.0