什么是Mono内存
Unity引擎托管堆内存是由Mono分配和管理的。“托管”的本意是Mono可以自动地改变堆的大小来适应你所需要的内存,并且适时地调用垃圾回收(Garbage Collection)操作来释放已经不需要的内存,从而降低开发人员在代码内存管理方面的门槛。
Unity游戏在运行时的内存占用情况可以用下图表示:
目前绝大部分Unity游戏逻辑代码所使用的语言为C#,C#代码所占用的内存又称为mono内存,这是因为Unity是通过mono来跨平台解析并运行C#代码的,在Android系统上,游戏的lib目录下存在的libmono.so文件,就是mono在Android系统上的实现。C#代码通过mono解析执行,所需要的内存自然也是由mono来进行分配管理,下面就介绍一下mono的内存管理策略以及内存泄漏分析。
Mono内存管理策略
Mono通过垃圾回收机制(Garbage Collect,简称GC)对内存进行管理。
Mono内存分为两部分,已用内存(used)和堆内存(heap),已用内存指的是mono实际需要使用的内存,堆内存指的是mono向操作系统申请到的内存,两者的差值就是mono的空闲内存。
当mono需要分配内存时,会先查看空闲内存是否足够,如果足够的话,直接在空闲内存中分配,否则mono会进行一次GC以释放更多的空闲内存,如果GC之后仍然没有足够的空闲内存,则mono会向操作系统申请内存,并扩充堆内存,具体如下图所示:
Mono中的GC主要有以下几个步骤:
1.停止所有需要mono内存分配的线程。
2.遍历所有已用内存,找到那些不再需要使用的内存,并进行标记。
3.释放被标记的内存到空闲内存。
4.重新开始被停止的线程。
除了空闲内存不足时mono会自动调用GC外,也可以在代码中调用GC.Collect()手动进行GC,但是,GC本身是比较耗时的操作,而且由于GC会暂停那些需要mono内存分配的线程(C#代码创建的线程和主线程),因此无论是否在主线程中调用,GC都会导致游戏一定程度的卡顿,需要谨慎处理。另外,GC释放的内存只会留给mono使用,并不会交还给操作系统,因此mono堆内存是只增不减的
。
Mono内存泄漏分析
Mono是如何判断已用内存中哪些是不再需要使用的呢?是通过引用关系的方式来进行的。Mono会跟踪每次内存分配的动作,并维护一个分配对象表,当GC的时候,以全局数据区和当前寄存器中的对象为根节点,按照引用关系进行遍历,对于遍历到的每一个对象,将其标记为活的(alive)。
如上图所示,假设A是处于全局数据区的一个对象,那么在GC的时候将作为根节点进行遍历,由于B、C、D对象都可以由A遍历到,因此被标记为活的,E、F对象则没有被标记。注意,由于引用关系是单向的,A引用了B并不代表B也引用了A,所以遍历也只能单向进行。
由于GC以全局数据区和当前寄存器中的对象为根节点进行遍历,所以对象的被标记意味着该对象可以通过全局对象或者当前上下文访问到,而没有被标记的对象则意味着该对象无法通过任何途径访问到,即该对象“失联”了,GC最终会将所有“失联”的对象内存进行回收,上图中的E和F将会在GC过程中被回收。
既然mono已经有了完善的GC机制,那是否还会存在内存泄漏呢?答案是肯定的,只是此处的内存泄漏需要重新定义一下,我们把对象已经不再需要使用却没有被GC回收的情况称为mono内存泄漏。Mono内存泄漏会使空闲内存减少,GC频繁,mono堆不断扩充,最终导致游戏内存占用的升高。下图就是一个mono内存泄漏的例子:
Mono内存泄漏解决办法
对于mono内存泄漏,一般只能通过猜测+不断修改代码测试的方法来修复问题,效率很低,下面结合具体的代码看一个内存泄漏的例子。
public class A
{
int[] a;
public A()
{
a=new int[1000];
}
}
static A objA;
void OnGUI(){
if(GUI.Button(new Rect(10,50,160,120),"测试")){
objA=new A();
}
}
上面代码我们定义类A,并在A的构造函数中申请了一块int[1000]大小的内存。通过点击按钮,会在内存中申请int[100]内存,并由静态对象objA引用。
需要注意的是,由于objA是静态对象,是GC的根节点,GC遍历是以它为其中的一个根节点,来判定其它对象是否没有被引用。因此GC无法直接回收objA,也就导致int[1000]无法被回收,从而导致Mono内存泄漏。
若想要被回收,可以设置objA.a=null。这样在GC遍历objA的时候是可以发现int[1000]属于未被引用的内存,从而被回收。(注意设置为null,并不能立即回收内存,只是断了引用关系,下一次GC的时候才会把这块没有引用的内存给回收)
Cube工具
腾讯Wetest平台的Cube工具提供了mono内存快照对比的功能,并包括对象分配堆栈,对象引用关系等详细信息,是定位mono内存泄漏问题的一大利器。
补充
据说Unity上Mono内存最好控制在30MB内(未看到具体文档说明,待补充)