golang 的内存分配类似于tcmalloc(全局缓存堆和进程所属私有缓存)内存分配策略,大致采用三层内存分配模式。其中主要分为mheap, mCentral, mCache.
mheap:直接向操作系统申请内存,申请对象以页为基础单位,同时还负责大内存的直接分配。
mCentral:作为上级mheap, 下级mCache的中间项,起承上启下的作用, 当下级mCache内存不够时,mCache向mCentral申请,mCentral不够时,向mheap申请。
mCache:与MPG并发调度模型中的P关联,每个P拥有一个mCache,由于单个P每个时刻只能调度一个goroutine,所以不存在多个goroutine同时共享一个mCache,也避免了分配内存时候的加锁。mCache主要负责小对象的内存分配。
关于变量到底分配到堆上还是栈上?
变量的具体分配位置是由编译器决定的,当编译器无法判断变量作用域(比如返回变量地址)和变量大小(make([]int, 0, n))的时候,通常会将变量分配到堆上。理论上,编译器会优先考虑将变量分配到栈上,因为栈上的变量,会在函数出栈后自行释放,而不需要gc的处理,缓解gc压力。
关于垃圾回收?
go的垃圾回收使用标记-清理的方式进行内存回收,在老版本中,go从标记阶段就开始stop the world,直到清理完成后,才start the world,导致冻结所有goroutine的时间过长,目前go 在标记阶段采用了和其他协程并发的方式运行, 只有在清理的时候才会stop the world,大大缩短了阻塞时间。
关于内存优化?
在使用map, slice的时候,由于他们会进行动态扩容,所以在使用的时候,如果能预知分配大小,尽量初始化大小,可以避免扩容。
当我们在频繁使用make([]int, 0, n)的方式来分配内存的时候, 内存分配会在堆上分配,虽然mCansh已经根据size class对内部的内存进行了分类管理,但是频繁和大小不定的内存申请,必然造成内存碎片,同时,当我们再次申请时,运行时会不断的遍历内存管理链表,以求获得相应的内存块,如果内存不够,还要向上级申请内存。如此,一个简单内存分配操作,却会消耗相当长的时间,拖慢运行时间。
这个时候,我们就可以使用临时对象池,sync.pool,来避免频繁的分配内存,复用临时对象,同时减少了gc压力。sync.pool为什么是临时对象池呢,因为放入sync.pool中的对象,在下一次gc的时候会进行全部释放,所有put进入的对象是保证不了生命周期的。
有疑问加站长微信联系(非本文作者)