Unity-GC详解
什么是GC呢?
简单的说,Unity的GC就是一个专门针对动态内存(堆内存)进行自动回收的机制(Garbge Collector)
但是这里,我们就要说清楚一些Unity和C#的更低层次的东西了:
我们都知道Unity是用C#写成的,或者说我们的源码是C#构成的,按照正常的流程,我们要将代码进行编译后再汇编才能变成机器可以阅读的机器码。其中关于这个编译方法主要有两种:
可以看到两种编译方式的主要区别就在于CIL(中间语言)到机器码之间的这个过程,Mono方案是生成JIT/AOT而IL2CPP是生成C++,具体区别如图所示,简单的说就是Mono最大的好处就是支持热更新,其他不论性能还是安全性IL2CPP都更高一筹。
所以我们来讨论IL2CPP的情况下我们的GC:
所以我们真正的主角就是:Boehm GC
比较直白地说,我们的Bohem GC的运行原理就是遍历所有内存区域(如线程栈、全局变量、寄存器),将其中 符合地址范围和对齐规则 的数值视为 潜在指针,这些潜在指针指向的动态内存就是活跃对象,最后将所有的不活跃内存回收。
Bohem GC和C#/JAVA的GC以及Lua的GC的区别:
Bohem GC优化点:
一般来说,GC只负责回收堆上的内存,但是我们如果换个角度想:我们什么时候会去回收内存呢?往往是我们在堆上分配了一块新的内存,虽然二者并不是强关联关系,但是确实客观上存在可能。
因此,我们现在去考虑考虑哪些场景会导致我们的GC:
首先,C#中涉及到堆,我们很容易想到:
装箱:
装箱时,我们需要将原本存储在栈上的值类型复制到堆上开辟的新内存上,变成引用类型。这个过程中涉及到堆内存的分配,比如我们要是频繁进行装箱的话就会引发GC。
针对这种情况,我们最好的优化思路就是去减少装箱:这是我们的一个核心理念。
比如我们的泛型:
匿名方法:
我们都知道匿名方法在通过闭包捕获外部变量时,会生成一个匿名类,既然涉及到类(引用类型)那自然就会涉及到堆内存的分配,那么自然就会涉及到GC了。
int n = 1;
void Test()
{Call(()=> n = 2);
}
那么要如何去优化呢?
一般来说,我们可以把匿名方法要捕获的对象写成静态的,这样就不用生成匿名类来存储这个捕获的变量了(静态变量等同于类,直接通过类名访问);
static int n = 1; // 静态变量
void Func() {Call(() => n = 2); // 匿名方法直接访问静态变量
}
我们还可以利用委托来缓存匿名方法,这样我们只用在第一次调用委托时生成匿名类,后续我们就一直调用委托缓存中的匿名方法,不会生成新的匿名类。
int a = 1; // 实例变量
Action action; // 缓存委托实例
void Func() {if (action == null) { // 首次初始化委托action = () => a = 2; // 生成闭包(仅一次)}Call(action); // 复用缓存的委托实例
}
字符串:
我们都知道C#中的string本身是不可变的,任何对string类型变量的操作都需要先复制一个源字符串再进行操作,那么这个时候旧的字符串就变成了无人使用的内存垃圾。
虽然当我们提到关于修改string类型对象的思路一般都是使用stringbuilder,但是实际上stringbuilder并没有那么好用,我们可以去使用一些其他的优化方法。
类和结构体:
这个就是基本都能想到的:结构体是值类型而类是引用类型,对于并不庞大的数据,能使用结构体的话优先使用结构体,不会涉及到堆上内存的分配就不会引发GC。
容器:
大多数我们使用的容器都支持动态扩容,那么每次动态扩容都会涉及到堆上的内存分配,所以尽可能地我们去减少容器的扩容。
Unity的GC和C#的GC对比: