课程名称:深入Go底层原理,重写Redis中间件实战
课程章节:9-3,9-4,9-5
课程讲师:Moody
课程内容:
★heapArena
Go每次申请的虚拟单元为64MB
最多有2的22次方 4,194,304 个虚拟内存单元
内存单元也叫heapArena
所有的heapArena组成了mheap(GO 堆内存)
32位系统给每个进程分配的是4g内存,而64位系统分配的是256T的虚拟内存
★如何使用heapArena
线性分配
线性分配器是一种高效的内存分配方式,只需要在内存中维护一个只想内存特定位置的指针,如果用户要内存,就检查剩余空闲内存,返回分配的内存区域并修改指针在内存中的位置。线性内存分配器有较低的实现复杂度,执行速度也很快。但是他无法利用已经分配过的内存。比如在整个“线”上,之前被分配的内存被GC回收释放,但是指针无法返回到被释放的内存位置,导致产生大量的内存碎片。
线性分配器的内存碎片是通过定期合并空闲内存来整理的,一定程度上还增加了复杂度
空闲链表分配
空闲链表分配器在线性分配的基础上, 依次遍历空闲内存块,并使用链表存储这些空闲内存的指针。再次使用空闲内存的时候,会遍历这个链表,时间复杂度是O(n)。空闲链表分配器可以选择不同的策略在链表中进行选择:
▲首次适应(first-fit)----从链表头部开始遍历,选择第一个空间大于申请内存的内存块
▲循环首次适应(next-fit)----从上次遍历结束的位置开始遍历,选第一个空间大于申请内存的内存块
▲最优适应(best-fit)----从链表头部开始遍历整个链表,选择最合适的内存块
▲隔离适应(segregated-fit)----将内存分割成多个链表,每个链表中的内存大小相同,申请内存的时候,先找到满足条件的链表,再从链表中选择合适的内存块,这就非常接近Go的内存分配策略了
分级分配
线程缓存分配--TCMalloc,比glibc的malloc还要快一些。其核心理念是使用多级缓存将对象根据大小分类,并按照类别实施不同的分配策略。
★分级分配
runtime.mspan 是go里面内存管理的基本单位,该结构体是可以组成一个链表-->runtime.mSpanList。
next,prev分别指向前一个mspan和后一个mspan
每个runtime.mspan 都管理着npages 个大小为8kb的页,这些页不是系统的内存页,是go的内存页,大小为系统内存页的整倍数。
▲startAddr和npages ----确定该结构体管理的多个页所在内存,每个页的大小都是8k
▲freeindex ---- 扫描页中空闲对象的初始索引
▲allocBits 和 gcmarkBits ---- 分别用于内存占用(alloc)和 回收(gc)的情况
▲allocCache ---- allocBits 的不码,可用于快速查找内存中未被使用的内存
运行时会使用runtime.mSpanStateBox 存储内存管理单元的状态 runtime.mSpanState
状态共有四种mSpanDead、mSpanInUse、mSpanManual 和 mSpanFree,当mSpan处于空闲堆中,就是mSpanFree,当被分配后,状态是mSpanInUse或mSpanManual。运行的时候遵循以下状态:
▲在GC任意阶段,可以从mSpanFree转换为mSpanInUse或mSpanManual
▲在GC清除阶段,可以从mSpanInUse或mSpanManual转换为mSpanFree
▲在GC标记阶段,不能从mSpanInUse或mSpanManual转换为mSpanFree
跨度Class
runtime.spanClass是mspan的类型,mspan有67+1种类型,每一种类型都会存储特定大小的对象,并包含特定数量的页数。也就是说,mspan会根据需要,切割成67种不同的类型。从8B到32KB。还有一种0类型,主要是用于存储超过32kb的对象。
spanClass除了存储类别ID,还会存储一个noscan的标志位,表示是否包含指针,GC会对包含指针的mspan进行扫描。
spanClass是一个Unit8类型的整数,他的前7位是存储的跨度Class的类型ID,最后一位表示是否包含指针,通过位运算看最后一位是否为1,为1则认为是有指针
这是什么意思呢,意思是需要gc扫描和不需要gc扫描,比如常量就不需要gc扫描
课程收获:
通过学习,明白了go内存分配的mspan的结构