Unity性能优化之内存管理的优化(Mono平台下的内存管理)

此部分分为以下几个环节来记录

Mono平台

  1. 本地和托管内存域
  2. 垃圾回收
  3. 内存碎片

IL2CPP

如何分析内存问题

不同内存祥光性能增强

  1. 最小化垃圾回收
  2. 正确使用值类型和引用类型
  3. 正确使用字符串
  4. 与unity引擎相关的许多潜在增强
  5. 对象和预制池

Mono平台

Mono是一种神奇的调味汁,使Unity具有很多跨平台的功能,其目标使通过框架提供跨平台开发,该框架允许用统用的编程语言编写代码运行在不同的硬件平台上,包括 Linux、MacOS、Windows、ARM等,Mono甚至支持很多不同的编程语言,可以编译为.NET的统用中间语言CLR的任何语言都能与Mono平台集成;

一个常见的错误概念就是Unity引擎使构建在Mono平台上的,因为基于Mono的层并没有处理很多重要的游戏任务诸如,音频、渲染、物理、以及时间的跟踪,Unity引擎处于速度的原因构建了本地C++后端,允许它的用户将Mono作为脚本编程界面,控制该游戏引擎,这和其他的游戏引擎一样,在底层运行C++,处理诸如渲染、动画、和资源管理这样的任务,而为要实现的玩法逻辑提供高级脚本语言;
脚本语言通常是通过自动垃圾回收抽象并且分离了复杂的内存管理机制,并且提供各种安全管理内存的特性,这些都是以习剩运行时的开销作为代价的,简化了编程的行为,这样的语言通常称为托管语言,从技术上讲,是指必须要在公共语言运行时(CLR)运行的源码;
CLR:实际上就是为高级脚本语言提供服务的类库,这些服务包括但是不限于

  1. 类加载器:管理元数据,加载和在内存的布局类
  2. 垃圾回收装置
  3. 线程机制
  4. .NETFramwork
  5. 类库支持
  6. 异常管理等
内存域

Unity引擎中的内存空间本质上可以划分为三个不同的内存与,每个域存储不同的数据类型,关注不同的任务集;

托管域

这个域是Mono平台工作的地方,我们编写的任何MonoBehaviour脚本和自定义的C#类在运行时都会在这个域实例化对象,它同时也被称之为托管域,因为它自动被垃圾回收管理;

本地域

Unity有一些底层的本地代码共嗯那个,由c++编写,并且根据目标平台编译到不同的应用程序中,该域关心内部内存空间的分配,如为各种子系统(诸如渲染管线、物理系统、用户输入系统)分配资源(例如纹理、音频文件、网格等)和内存空间,实际上他也包括GameObject和Component等重要游戏对象的部分本地描述,也是大多数内建Unity类(Transform和Righdbody组件)保存其数据的地方;

本地托管桥的由来: 当和Tansform等组件进行交互的时候,大多数指令会请求Unity进入其本地代码,在哪里生成结果,接着将结果复制回托管域,由于跨越这两个域需要内存进行上下文的切换时,回个游戏带来许多潜在的性能问题,所以应该尽可能的最小化此行为;

外部库

常见的外部库如DirectX、OpenGL,这个域还包括项目中很多的自定义库和插件,并且在C#代码中引用这些类库将导致类似的内存上下文切换 和后续成本;

在现在大多数操作系统中,运行时的内存空间分为两种类型,栈和堆

栈是内存中预留的特殊空间,专门用来存储小的短期的数据值,这些数据一旦超过作用域就会自动释放,其包含了已经生命的任何本地变量,并在调用函数的时候处理他们的加载与卸载,这些函数就是通过调用栈来进行扩展与收缩的;

堆表示所有其他的内存空间,由于在运行的时候,我们像有大多数内存分配的持有时间要比当前函数的持有时间长,因此不能在栈上分配他们,在本地代码中,例如用C++编写的代码,这些内存是通过手动处理的,我们有责任去确保正确的分配所有内存块,并且在不需要的时候进行释放,如果没有正确的去处理内存释放,那么很容易通意外的发生内存泄漏,因为可能会持续的从RAM中分配越来越多的内存空间,却从不清理,直到没有内存可以分配,甚至程序崩溃;

垃圾回收

垃圾回收的三种情况:

  1. 堆内存空间足够:当请求新的内存空间的时候,如果托管的堆内存中有足够的空闲空间可以满足该请求,GC只是简单的分配新的空间并交给调用者;
  2. 堆内存空间不够:如果当前托管的堆内存中没有足够的空闲空间,那么GC将扫描所有已经存在的且不再使用的内存分配兵器清除他们;GC会把拓展当前对空间作为最后的手段;

Unity所使用的Mono版本中的GC是一种追踪GC,它使用标记与清除策略,该算法分为两个阶段,每个分配的对象通过一个额外的数据位追踪,该数据位标识对象是否被标记,如果这些标志位被设置为false,标识它尚未被标记;

当收集过程开始的时候,它通过设置对象的标识为true,标记所有依然堆程序可访问的对象,可访问的对象要么是直接引用,要么是通过直接或者间接的可访问对象的字段来间接引用,本质上,它收集一系列的被程序引用的对象,对程序而言,任何没有被引用的对象都是不可见的,而这些对象是可以被GC回收的;
第二个阶段涉及迭代这类引用,根据对象是否被标记决定是否回收,如果对象被标记,那么在某个地方还是在引用它,GC会掠过这类对象,如果没有被标记,直接回收,被标记的对象会在下次GC扫描前设置为false;
一旦第二个阶段结束,所有没有被标记的对象都得以回收以释放空间,如果已经为对象释放了足够的空间,那么在新释放的空间中分配内存并返回给调用者;但是如果空间不够,只能通过向操作系统请求拓展托管堆了;
在理响情况下,我们持续的分配和回收对象,但是一次只能处理有限数量的对象,堆将保持大致恒定的大小,但是程序中的所有对象很少以他们分配的顺序被回收,而且他们占用的内存大小很少一样,这就会导致内存碎片;

内存碎片

常见堆内存空间的内存分配与回收

内存分配如下:
以空的堆空间开始
在堆上分配四个对象ABCD,每个大小为64字节
后来回收其中两个对象AC,释放128字节
尝试分配128字节的大对象
由于回收了AC两个64字节大小的对象,现在要分配128字节的对象,但是已有的内存空间不连续,无法将128字节对象存入,因此,对于这两个64字节的空间来说,除非有小于等于64的对象需要在堆空间内存入,否则不会再重用;
那么这种情况下去,随着不同大小的对象被回收,堆空间可能会充满更多、更小的不连续的空间空间,这也就是内存碎片产生的原因,所导致的问题有两个,首先,从长期来看,这种现象显著的减少了新对象的总可用内存空间,者取决于分配和回收的频率;其次,它使新的分配花费的处理时间更长,因为需要花费额外的时间查找足以容纳对象的新内存空间;

运行时的垃圾回收

因此,在最坏的情况下,当游戏请求新的内存分配的时候,CPU在完成分配之前需要花费CPU周期完成下面的任务

  1. 验证是否有足够的连续空间用于分配新对象
  2. 如果没有足够的空间,迭代所有已知的直接和间接引用,标记他们是否可达
  3. 再次迭代所有这些引用,标识未标记的对象用于回收
  4. 再次迭代这些引用,以检查回收一些对象是否能为新的对象创建足够大的连续空间
  5. 如果没有,从操作系统请求新的内存块,以便扩展堆
  6. 在新分配的块前面分配新的对象,并返回给调用者
    此时,CPU需要处理很多工作,例如,当该新内存分配给重要对象,如粒子特效、场景中的新角、或者切换场景过度的时候,用户可能会注意到,此时GC冻结了游戏以处理挤断的情况;另外,GC会随着所分配的堆空间增长而变差,因为擦除几兆空间要比扫描几千兆字节空间快得多;
多线程的垃圾回收

GC运行在两个独立的线程上,主线程和Finalizer Thread(终结线程),当调用GC的时候,它运行在主线程上,然后标记堆内存块为后续回收,但是这不会立即发生,实际上,由Mono控制的Finalizer Thread在内存最终释放并且可用于重新分配之前,可能会延迟几秒;
因此,内存一经释放就可以回收这一概念时不对的,所以,不因该浪费时间尝试去消耗可用内存的最后一个字节,必须要有某种类型的缓冲区来用于未来的分配;
GC释放的块有时会在一段时间之后返回到操作系统,这将减少堆消耗的保留空间,并允许内存分配给其他对象,但是这是不可预测的,它取决于目标平台,因此不应该依赖它,唯一的安全假设就是,一旦内存分配给Mono,它就会被保留,不再可用于本地域或相同系统上运行的任何其他程序;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值