Go 贡献者 minux.ma 关于内存泄漏问题的详细解释

目前Go使用的GC是个保守的GC,换句通俗的话说就是宁可少释放垃圾不可误释放还在用的内存;这个反映在设计上就是会从堆栈、全局变量开始,把所有可能是指针的字全部当作指针,遍历,找到所有还能访问到的内存中的对象,然后把剩下的释放。

那么如何判断一个字(uintptr)可能是指针呢?大家知道Go的内存分配是参考的tcmalloc,并做了一些改动,原先tcmalloc是使用类似页表的树形结构保存已经从操作系统中获得的内存页面,Go使用了另外一个办法。由于Go需要维护每个内存字的一些状态(比如是否包含指针?是否有finalizer?是否是结构体的开始?还有上面提到的是否还能访问到的状态),综合在一起是每个字需要4bit信息;于是Go就先找一片区域(arena),以不可访问的权限从操作系统那里申请过来(mmap的prot参数是PROT_NONE),然后根据每一个uintptr对应4位申请一片RW的内存(bitmap)与前面的arena对应;这样已知heap上内存的地址想获得对应的bitmap地址就很简单了,不需要像tcmalloc似的查找,直接简单的右移和加法就能获得;同时呢,操作系统的demand paging会自动处理还没有使用到的bitmap。这里大家就明白了为啥Go用了那么大的虚拟内存(arena)并且知道为啥经常在内存不足的时候panic说申请到的内存不在范围了(因为内存不在bitmap所能映射的范围里,当然多个bitmap是可以解决这个问题的,不过目前还不支持);回到开始的那个问题,既然arena有个地址范围,判断一个uintptr是否可能是指针就是判断是否在这个范围里了。

这样的问题就来了。如果我有一个int32,他的内容恰巧在那个范围里,更碰巧的是如果把它当作指针,它恰巧指向一个大的数据结构,那么GC只能认为那个数据结构还在使用中。这样就造成了泄露。这个问题在32位/64位平台上都是存在的。但是在32位上问题更严重些,主要是32位表示的地址空间有768MB是Arena,也就是说一个均匀分布的uintptr是指针的概率是768/4096,这个远比64位系统的16GiB/(2^64B)的概率大得多。

Go 1.1不出意外的话会使用记录每个heap上分配的对象的类型的方式来几乎完整地解决这个问题;说几乎完整是因为,堆栈上的数据还是没有类型的,所以这里面还是有前面说的问题的,只不过会影响会小很多了