一、清理
当垃圾标记完成后,接着就以内存块(span)为单位进行清理操作;其实这里会带来一个疑问:是不是要挨个检查所有的内存单元,然后在一一清理。在理解这个东西之前需要看看mspan的结构
type mspan struct{ // 代表一个内存块
//...
gcmarkBits *gcBits // 标记位图; 对应的object标记为垃圾 等待清理
allocBits *gcBits // 分配位图; 对应的内存块object使用情况
//...
}
通过上面的源码可以看到有一个垃圾标记位图(gcmarkBits),垃圾回收器以此标记出可回收,也就是可被复用的内存位置(golang分配的内存块当在进行垃圾回收时,并不会直接归还给操作系统,而是完成清理后归还给central中间部件,以便缓存部件能够复用,减少频繁的从操作系统分配内存的性能损耗),由于gcmarkBits标记位图和allocBits分配位图两者相似性,可以通过直接将标记位图数据复制给分配位图即可实现垃圾清理工作,关于内存单元里遗留数据的清除与否可以交给分配操作来考虑:在gcmarkBits标记位图里面对应bit=1代表当前位置object已被标记为垃圾,allocBits分配位图中对应bit=1代表当前object位置已被使用。
接下来看看垃圾回收具体操作:
func (s *mspan) sweep(preserve bool) bool{
spc := s.spanclass // 内存块规格
// 已标记的已分配object数量(不包含可回收部分)
nalloc := uint16(s.countAlloc())
// 需要判断下当前大小规格:因为小对象和大对象分配来源不一样,大对象归还给堆
if spc.sizeclass() == 0 && nalloc == 0{
freeToHeap = true
}
// 本次回收object数量 = 标记分配总数量 - 标记后分配数量
nfreed := s.allocCount - nalloc
// 内存垃圾清理 会带来内存块属性的调整
s.allocCount = nalloc // 去掉被回收的object部分
s.freeindex = 0 // 空闲索引置0
// 将标记位图的记录当成分配位图的内容 实现复制
s.allocBits = s.gcmarkBits
s.gcmarkBits = newMarkBits(s.nelems)
// 小对象分配内存块直接归还给中间部件mcentral,以便复用给其他的mcache
if nfreed > 0 && spc.sizeclass() != 0{
res = mheap_.central[spc].mcentral.freeSpan(s, preserve, wasempty)
} else if freeToHeap{ // 大对象被分配内存块直接归还heap堆
mheap_.freeSpan(s, 1)
}
}
在上述代码可以看到针对不同的大小object,golang采用了不同的内存回收策略,一般来说小对象多使用频繁,故而采用将回收的内存块归还给中间部件mcentral以便其他的mcache使用,减少内存分配的频繁操作以及内存复用; 而大对象相对来说是比较少的分配的内存块比较大,那么就直接从heap堆分配,回收时直接归还给heap堆。接下来看看对应的组件的回收操作
func (c *mcentral) freeSpan(s *mspan, preserve bool, wasempty bool) bool{
//...
if preserve{ // 调整存储列表
return false
}
if wasempty{
c.empty.remove(s)
c.nonempty.insert(s)
}
// 更新垃圾回收代龄(缓存部件mcache是作为垃圾回收的跟对象)
atomic.Store(&s.sweepgen, mheap_.sweepgen)
// 还存在已分配内存 则直接返回 不需要进行内存回收 上交heap
if s.allocCount != 0{return false}
// 内存全部回收 则需要对应的内存块上交给heap
c.nonempty.remove(s)
mheap_.freeSpan(s,0)
//...
return true
}
在前面关于小对象的回收 只提到将对应的分配内存归还给中间部件(mcentral),那当中间部件(mcentral)持有的内存块回收全部空间的话,则需要归还给heap堆,这样可以便于其他的中间部件(mcentral)使用,这样也保证资源平衡,使得不同大小规格的内存请求能充分已有的内存,而不是重新向操作系统申请。
比如mcentral1持有大量的闲置内存块,而此时mcentral2、mcentral3等因内存耗尽发出扩容申请,这时候就可能到heap向操作系统进行分配物理内存申请。这样会导致更多的内存被闲置、浪费;那么若是将mcentral1闲置内容归还给heap堆,进而能够转给mcentral2、mcentral3使用是不是可以带来内存复用,尽量减少上述问题的产生。可能上交的内存块存在遗留数据,这就需要分配操作来完成。
这里面有点需要注意:mcentral上交的是完整回收空间span,主要由于每个span仅服务一种size class大小规格的对象,若是将剩余空间转给其他的mcentral,就会带来切分的操作,这样更容易导致内存碎片化。
回收操作
func (h *mheap) freeSpan(s *mspan, acct int32){
systemstack(func(){
h.freeSpanLocked(s, true, true, 0) // 回收 并尝试合并
})
}
有疑问加站长微信联系(非本文作者)