go语言的内存分两部分,一部分用作堆,供内存分配用,另一部分是bitmap,用来管理堆。两部分从同一地址开始,向高地址方向增长的是内存池,向低地址方向增长的是bitmap。
内存分配
对于较大的内存申请,直接从堆上申请,释放时也直接返回给堆。
而当一个go routine申请小于32k字节的内存,则从go routine私有的内存池中分配内存。因为是私有的,所有在多数情况下,分配内存不需要上锁。如果私有内存池没有内存了,则需要向中心内存池申请内存,中心内存池是共享数据,此时需要上锁。如果中心内存池也没有内存了,则从堆里申请内存。
私有内存池和中心内存池里的内存都是按照大小分开管理的,这样,分配和释放内存都非常快,而且也不容易产生碎片。通常,中心内存池从堆上申请一大块内存,然后将其打碎成多个同样大小的块,并串成一个free list。而当私有内存池向中心内存池申请内存时,也会一次性多要一些内存块,供以后使用,避免私有内存池和中心内存池之间过于频繁地交互,增加上锁的代价。
而当私有内存池内有过多的剩余内存时,会将其中一部分还给中心内存池,供其它go routine使用,以此避免其它go routine在有内存的时却申请不到内存的情况。
看完这部分代码,让我联想到Is Parallel Programming Hard第四节Counting的讲解,虽然它讲的是同步而非内存管理,但是由于结构类似,让我很快就理解了这部分内存管理的实现。
垃圾收集
对于每一个堆上的word,在bitmap上有对应的四个bit,记录了该word是否已经被分配、是否是一个边界、是否被标注等等。开始垃圾收集时,先标注所有全局变量,然后标注所有go routine的栈,并从这些被标注的对象开始,递归标注所有它们引用的对象,当这个过程停止后,所有活着的对象都被标注好了,而那些已经被分配但是没有被标注的对象所占用的内存则可以释放了。这是一个经典的mark-sweep算法。
gc启动的条件是每次分配内存之后检查,
if(dogc && mstats.heap_alloc >= mstats.next_gc) runtime·gc(0);
而在gc之后会更新next_gc
mstats.next_gc = mstats.heap_alloc+mstats.heap_alloc*gcpercent/100;
如果gcpercent为100,而本次gc后堆分配了4M,则下一次gc为堆增长到8M。参见gcpercent变量定义之前的注释。
由于引用未必都是指向一个对象的首地址,比如可以指向一个结构中的某个域,那么如何获得该域所在对象的首地址和大小呢。bitmap中的边界位就是为了回答这个问题。从引用所指向的地址向两边找,如果碰到对应的边界位被置上,则到达了对象的边界,这样就确定引用对象的首地址和大小。
go的正则表达式太丑陋了。