Unity的内存结构
首先,我们应该都知道,Unity是一个C++引擎,而原生支持的编程语言一般是c#(毕竟还有js)。一般来说,项目开发中,还会再有一种脚本语言支持,常见的是lua,typescript,c#。当然,还有些项目会加一些C++的项目底层,不过不是普遍情况,而且也不是必需的,所以暂且不论。
所以Unity代码逻辑从最底层开始,就被划分成了四块。最底层的C++底层,上层的Unity写的C#Binding层(以前这里甚至不是C#,而是自己的语言),项目的C#层,项目的脚本语言层。
而Unity的内存空间,也可以用类似的性质分成3个不同的内存域:托管域,本地域以及外部域。
- 托管域:这是Mono(IL2CPP)平台工作的地方,也是我们重点关注的地方。大部分情况我们都在这里进行性能优化,和GC,实例化等搏斗。“托管”的本意是Mono可以自动地改变堆的大小来适应所需要的内存,并且适时地调用GC(Garbage Collection)操作来释放已经不需要的内存。当然,不少朋友可能知道Mono和Coreclr的事情。不过这个迁移变化不改变这个结构。打包选择哪种也无关这部分结构。
- 本地域:Unity底层使用,主要涉及引擎内部的内存空间分配,比如渲染,物理等。也是大多数原生Unity类,比如Transform保存数据的地方(所以这里有些我们觉得习以为常的操作,其实就和lua跨域访问c#一样,是跨桥操作,是有性能损耗的,具体会在下一章性能优化上讲解)
- 外部域:DirectX、OpenGL以及项目中包含的自定义库和插件所使用的内存域,在C#代码中引用这些库将导致类似的内存上下文切换和后续成本。
也许有人会说,那脚本层呢?那个Unity管不到了(华佗没仔细研究过,不清楚在不在托管里,大概率不在?)同样Unity检测不到的还有C++插件这类用户分配的Native内存
Unity本地域
我们知道unity是个c++引擎,unity的底层也重载了内存分配的的操作符,分配的时候会多指定一个参数memory lable,来分配到指定的内存池。
当加载进内存的时候会生成一个NewAsRoot,其附属的数据就会作为它的成员去依次分配。
需要注意的是,本地域因为是c++,内存的释放是手动管理的,所以会及时返还给系统,这点和托管域不同。
Unity托管域
Unity托管域,也就是VM内存池。我们需要讨论的也就是两块,一个是怎么申请,一个是怎么返还。
Unity托管域申请
Unity托管域一开始启动的时候,会申请一定量的连续内存,供自己内部使用,如果空余内存不够了,引擎才会向系统再次申请一定量的连续内存进行使用。
当用户向Unity申请托管堆内存的时候,托管堆会首先检查当前堆内的空间内是否存在足够的连续空间。如果找不到足够的连续空间,那么就会进行一次GC。
如果在GC运行之后,仍然没有足够的连续空间来容纳,托管堆就会执行内存拓展操作,向操作系统申请更多的内存。每次拓展的大小因平台而异。
Unity的GC
既然申请过程中提到了GC,那我们也来说一下Unity的GC,严格来说,Unity现在有三种GC模式。
- 原始的Mono模式,用的是Mono的GC,即Boehm,一种非分代(Non-generational),分压缩式(Non-compacting)的GC
- Unity的IL2Cpp不用Mono的GC了,自己写了一个,因为黑盒,只知道是更激进的Boehm.
- 现在如果我们要进行一次GC,主线程被迫要停下来。也就是上一章提到的Stop the World。遍历回收。所以Unity做了一个分帧优化,也就是Incremental GC(渐进式 GC)
Unity的内存归还
当开发者归还内存给Unity的托管域后,上文有提到,Unity托管域是不会及时返还给系统的,根据Unity高川老师的说法,一块内存如果连续六次没被命中,才会被返回。