一、内存构成
二、扩容
一般来说当内存空间span不足时,需要进行扩容。而在扩容前需要将当前没有剩余空间的内存块相关状态解除,以便后续的垃圾回收期能够进行扫描和回收,接着在从中间部件(central)提取新的内存块放回数组中。
// 根据空间规格 获取对应的索引 再获取alloc数组中对应的span
func (c *mcache) refill(spc spanClass){
s := c.alloc[spc]
if s != &emptymspan{ s.incache = false} // 解除当前span的状态
s = mheap_.central[spc].mcentral.cacheSpan // 从中间部件获取新的span
c.alloc[spc] = s //放回到数组
}
需要注意由于中间部件有scan和noscan两种类型,则申请的内存空间最终获取的可能是其两倍,并由heap堆进行统一管理。中间部件central是通过两个链表来管理其分配的所有内存块:
1、empty代表“无法使用”状态,没有剩余的空间或被移交给缓存的内存块
2、noempty代表剩余的空间,并这些内存块能够提供服务
有可能在垃圾回收后 还有些span内存空间没有被收回,但这并不影响其他线程来使用;由于每个内存块都有与垃圾清理相关的状态,当需要和垃圾回收期进行交互时,会关系到该如何复用每个内存块。
由于golang垃圾回收器使用的累增计数器(heap.sweepgen)来表达代龄的:
span.sweepgen =
sweepgen - 2 :代表当前内存块已被标记为垃圾 需要清理
sweepgen - 1 : 代表当前被标记为垃圾的内存块 正在清理
sweepgen : 代表为垃圾的内存块已完成清理,可再次使用
从上面内容可以看到每次进行清理操作时 该计数器 +2
再来看下mcentral的构成
type mcentral struct{
spanclass spanClass // span规格
nonempty mSpanList // 可使用的span
empty mSpanList // 不可使用的span
}
当通过mcentral进行空间span获取时,第一步需要到noempty列表检查剩余空间的内存块,这里面有一点需要说明主要是垃圾回收器的扫描过程和清理过程是同时进行的,那么为了获取更多的可用空间,则会在将分配的内存块移交给cache部件前,先完成清理的操作。第二步当noempty没有返回时,则需要检查下empty列表(由于empty里的内存块有可能已被标记为垃圾,这样可以直接清理,对应的空间则可直接使用了)。第三步若是noempty和empty都没有申请到,这时需要堆进行申请内存的
// 分配一块span便于在MCache中使用
func (c *mcentral) cacheSpan() *mspan {
retry:
var s *mspan
// 遍历noempty列表中有剩余空间的内存块
// 先执行清理工作 便于申请更多的空间
for s = c.nonempty.first; s != nil; s = s.next {
// 当前span的状态:进行垃圾清理
// 修改当前span的代龄状态
if s.sweepgen == sg-2 && atomic.Cas(&s.sweepgen, sg-2, sg-1) {
// 移除noempty列表中的记录
c.nonempty.remove(s)
// 添加empty列表的记录
c.empty.insertBack(s)
unlock(&c.lock)
s.sweep(true) // 清理
goto havespan
}
if s.sweepgen == sg-1 {
// 可能正被bgSweep或cacheSpan处理 则忽略该状态的span
continue
}
// 直接使用
c.nonempty.remove(s)
c.empty.insertBack(s)
unlock(&c.lock)
goto havespan
}
// 当noempty没有可用空间了 则需要从empty列表清理出可用空间
for s = c.empty.first; s != nil; s = s.next {
if s.sweepgen == sg-2 && atomic.Cas(&s.sweepgen, sg-2, sg-1) {
// 清理空间,并转移到empty列表尾部
c.empty.remove(s)
c.empty.insertBack(s)
unlock(&c.lock)
s.sweep(true)
// 查看是否有可用空间
freeIndex := s.nextFreeIndex()
if freeIndex != s.nelems {
s.freeindex = freeIndex
goto havespan
}
lock(&c.lock)
// 可能会出现清理后仍没有可用剩余空间
// 那就再重试noempty,继续前面的过程 直至找到可用的空间
goto retry
}
if s.sweepgen == sg-1 { // 正在被清理 跳过
continue
}
// already swept empty span,
// all subsequent ones must also be either swept or in process of sweeping
// 被移交给cache或清理后没有剩余空间的都会被追加到empty尾部
// 对应的sweepgen == sg(已清理)
// 遇到上述的内存块时, 表示后续无需再检查,直接跳出循环
break
}
// ......
// 从heap扩容 并直接返回.
s = c.grow()
if s == nil {
return nil
}
lock(&c.lock)
c.empty.insertBack(s)
unlock(&c.lock)
havespan:
// 更新span状态
s.incache = true
freeByteBase := s.freeindex &^ (64 - 1)
whichByte := freeByteBase / 8
// 分配位图
s.refillAllocCache(whichByte)
// 调整allocCache
// 使得span的freeindex处于allocCache的地位
s.allocCache >>= s.freeindex % 64
return s
}
通过上面的源码也可以看到中间部件central自身扩容操作与大对象内存分配差不多类似。
三、关于golang中微小对象(tiny)
在golang中将长度小于16bytes的对象称为微小对象(tiny),最常见的就是小字符串,一般会将这些微小对象组合起来,并用单块内存存储,这样能够有效的减少内存浪费。
当微小对象需要分配空间span,首先缓存部件会按指定的规格(tiny size class)取出一块内存,若容量不足,则重新提取一块;前面也提到会将微小对象进行组合,而这些组合的微小对象是不能包含指针的,因为垃圾回收的原因,一般都是当前存储单元里所有的微小对象都不可达时,才会将该块内存进行回收。
而当从缓冲部件cache中获取空间span时, 是通过偏移位置(tinyoffset)先来判断剩余空间是否满足需求。若是可以的话则以此计算并返回内存地址;若是空间不足,则提取新的内存块,直接返回起始地址便可; 最后在对比新旧两块内存,空间大的那块则会被保留。
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer{
if size <= maxSmallSize{
if noscan && size < maxTinySize{ // 是否使用微小对象分配器
off := c.tinyoffset // 当前内存块(cache.tiny)分配位置
// 剩余空间能够满足本次分配请求
if off + size <= maxTinySize && c.tiny != 0{
x = unsafe.Pointer(c.tiny + off) // 计算内存地址
c.tinyoffset = off + size // 调整下次分配位置
return x
}
// 剩余空间不足 则从cache中新取一块空间 根据空间规格spanclass申请
span := c.alloc[tinySpanClass]
v := nextFreeFast(span)
if v == 0{v,_,shouldhelpgc = c.nextFree(tinySpanClass)} //
// 直接获取起始地址
x = unsafe.Pointer(v)
(*[2]uint64)(x)[0] = 0
(*[2]uint64)(x)[1] = 0
// 对比新旧两块tiny内存 保留更大的那块
if size < c.tinyoffset || c.tiny == 0{
c.tiny = uintptr(x)
c.tinyoffset = size
}
}
}
}
在golang需要分配空间的object分三类:
1、零长度对象
2、小对象:需要先从缓存部件 接着中间部件 最后才是堆上申请
3、大对象:直接从堆heap上进行空间申请
有疑问加站长微信联系(非本文作者)