单源读取-GC导致内存泄漏问题

本文主要记录了Unity游戏中因为Mono源代码中的GC问题必然会出现一些内存泄漏问题的深层原因,涉及Mono源代码中GC机制的逻辑。

要想有这种内存泄露,首先要准备一个任意的内存块:(没有任何外部引用,理论上用完之后应该是GC,但是在这个BUG下会被错误泄露,不会被GC丢弃)

注意:大小是任意的,越大越容易被泄露。

struct结构的数组:(必须有一个类似指针的值类型,如int,以及另一个引用类型,如string)。

注意:大小是任意的,数组中的元素越多,越容易触发泄漏。例如,hashset

通过在GC中打点,用GDB调用GC进程,可以观察到所有对象的分布和GC的进程。发现缓冲区对象被槽对象错误引用,导致缓冲区对象正常GC失败,内存泄漏。

首先,对于mono/il2cpp的Boehm GC库,mono/il2cpp在分配内存时有几种类型的对象:

在这种情况下,使用NORMAL类型分配槽,使用PTRFREE类型分配缓冲区对象。

所以在做GC的时候,对于slots对象,GC会扫描对象的内存区间来寻找其内部的指针地址,也就是从0xde45f000到0xde468c50地址以指针对齐的方式寻找指针地址:

例如:0x de 45 f 000 0x de 464d 440 xde 464 f 400 xde 46513c。....

0xde464f40的地址值恰好是:0xbe82f000(即Slot结构中hashCode的值),GC会错误地把这个int值作为指针,而指针正好指向GC管理的一个内存块,也就是缓冲区对象,所以GC认为缓冲区对象被slots对象引用,缓冲区对象也被GC标记,不会释放。

这个问题的关键在于,GC错误地把slot结构中的hashcode当成了指向另一个托管对象的指针,于是GC错误地认为两个对象之间存在引用关系,导致内存泄漏。

最小化演示:

将结构槽修改为类槽可以修复内存泄漏问题,因为类对象的内存分配是类型化的。

由于Mono的GC设计问题,在Unity游戏中随着时间的推移几乎不可避免的会出现内存泄漏,因为这个问题会出现在HashSet等数据结构中。但我们能做的还是内存使用的两个真相(尤其是虚拟机类型的语言):

这样做并不能完全避免Mono的底层GC问题,但可以让这种内存泄漏更加温和。