可以把GC想象成一个描述程序对象之间互相引用的算法。如果在程序中一个子对象被其父对象引用(在原生代码中使用的是指针),那他们的关系图如下:
当GC对一个进程的内存进行扫描,它会尝试寻找没有父亲的对象。如果找到,就对他们进行回收以便释放内存给其他需求使用。
当然,其实大部分的对象都有父对象,因此GC需要确切的知道哪些对象是重要的父对象,这些对象是你的程序中真正正在使用的。用GC的术语来说,这些对象叫做“根”。下面的例子展示了两种不同的情况:
在上面的图片中,Parent2没有根对象,因此GC可以释放Parent2和其孩子Child2的内存以便重新使用。但是Parent1和Child1的情况不同,Parent1有根对象,因此GC不能回收他们,因为他们还在被程序使用。
对于.NET来说,有三类根对象:
- 在托管代码线程栈上的局部变量
- 静态变量
- GCHandle 对象
在托管代码线程栈上的局部变量
当一个线程被注册到垃圾回收器中,GC会把这个线程栈中的所有对象当成“根”对象。让我们看下那个线程中生成出来的原生代码(在HelloWorld_AnotherThread_m4函数中):
AnyClass_t1 * L_0 = (AnyClass_t1 *)il2cpp_codegen_object_new (AnyClass_t1_il2cpp_TypeInfo_var);
AnyClass__ctor_m0(L_0, /*hidden argument*/NULL);
V_0 = L_0;
我们能看到一个局部变量: L_0。GC必须将其视为“根”对象。在这个线程的短暂的执行过程中,这个“AnyClass”对象和其所引用的其他对象所占用的内存空间不能被GC回收另作他用。绝大部分在栈上的变量对于GC来说都是“根”对象,因为这些变量所在的函数都是在一个线程中被执行的。
当线程退出的时候,il2cpp_gc_unregister_thread函数被调用,用来告知GC这些对象已经不是“根”对象了。因此GC可以通过回收L_0来释放AnyClass所占用的内存空间。
静态变量
有些变量不在线程调用栈上。这些静态变量也需要被当成“根”对象处理。
GCHandle对象
假设你不想使用静态变量,但是你又想当GC回收他们的时候对变量有更多一些的控制权。特别的,当你将托管代码中的一个对象传递给原生代码,而原生代码又要保持这个对象的时候,我们必须告诉GC这些对象是“根”对象,不能被回收。这个是通过一个特殊的GCHandle对象来实现的。
GCHandle的创建告诉运行时代码这些对象应当被当成“根”对象处理,此对象以及其引用到的对象都不能回收重用。
IL2CPP运行时是如何和GC交互的:
通过“根”对象的概念让GC知道哪些对象可以被回收,而哪些不行。
Unity的垃圾收集
垃圾收集器定期运行(注意:确切的时间与平台有关)。这将扫描堆上的所有对象,标记删除任何不再引用的对象。然后删除未引用的对象,释放内存。
至关重要的是,Unity的垃圾收集 - 使用Boehm GC算法 - 是Non-generational和非压缩的。“Non-generational”意味着GC在执行收集传递时必须扫描整个堆,因此其性能因堆扩展而降低。“非压缩”意味着内存中的对象不会重新定位,以便关闭对象之间的间隙。
上图显示了内存碎片的示例。释放对象时,将释放其内存。但是,释放的空间也不会成为“空闲内存”的一部分。释放对象两侧的对象可能仍在使用中。因此,释放的空间是存储器的其他部分之间的“间隙”(该间隙由图中的红色圆圈表示)。因此,新释放的空间仅可用于存储与释放的对象相同或更小的数据。
分配对象时,请记住该对象必须始终占用内存中的连续空间块。
这导致了内存碎片的核心问题:虽然堆中可用空间的总量可能很大,但是该空间中的一些或全部可能在分配的对象之间存在小的“间隙”。在这种情况下,即使可能有足够的总空间来容纳某个分配,托管堆也找不到足够大的连续内存块来适应分配。
但是,如果分配了大对象并且没有足够的连续可用空间来容纳对象,则如上所述,Unity内存管理器执行两个操作。
首先,如果还没有这样做,垃圾收集器就会运行。这会尝试释放足够的空间来完成分配请求。
如果在GC运行后,仍然没有足够的连续空间来满足请求的内存量,则堆必须扩展。堆扩展的具体数量取决于平台;