golang对象内存分配

神奇的考拉 · · 4242 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

一、分配对象(源码)

// 分配对象内存入口
func newobject(typ *_type) unsafe.Pointer {
    return mallocgc(typ.size, typ, true)
}

// 分配指定object的大小(bytes数)
// 当分配的object大小 <= 32kb  使用每个P本地缓存空闲列表即可
// > 32 kB 直接堆上进行分配.
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
    // 零长度对象
    if size == 0 {
        return unsafe.Pointer(&zerobase) // 在heap上分配,则都指向预设的同一全局变量(零长度的object的地址均相同)
    }
       // 预分配对象大小 <= 32KB
    if size <= maxSmallSize {
        if noscan && size < maxTinySize {// 微小对象分配
            // 微小对象分配时则会将多个微小内存分配请求合并到一个单独的内存块
            // 当这些子对象变得不可达时,则会被释放掉
            //  这些子对象必须是不可扫描(没有任何关联的指针),这样能够确保内存浪费的可能性降低.
            // 
            // 关于maxTinySize组合后内存大小是可调的,目前默认设置=16bytes,在组合的对象中仅有一个对象是不可达的情况会造成近2倍左右内存浪费
            // 若设置=8bytes将基本不会带来内存浪费,但是合并的可能性就会降低
            // 若设置=32bytes有可能会带来大约4倍的内存浪费,同时也提供更多的组合机会
            // 所以这一块的微小对象组合阈值设定时 最好能够保持是8的倍数:8x
            // 
            // 需要注意:从微型分配器获取的对象是不能直接显示释放的
            // 若是想释放获取的对象,则需要该对象的大小 >= maxTinySize.
            //
            // 微型分配器面向的分配对象小的string和单独转义的变量
            // 以json为基准的性能,微型分配器带来了减少12%分配量
            // 并降低了大约20%堆大小
        } else {  // 小对象分配  
            // 代码省略
            // 获取对应class对应size等级=> size和spc
        }
    } else { // 大对象分配
        //  代码省略
        //  一般来说大对象数量相对比较少,生命周期比较长在内存中复用的可能性较低
        // 大对象所占内存不能算作碎片,这也是为嘛把大对象单独提出来处理的原因
        // 在Go中自定义栈大小=1GB,对象的分配默认优先在栈上而非heap堆
        // 在堆上分配时,直接从heap中获取大小合适的内存块,大块内存都是以页为单位
    }
}

二、关于内存的管理

其实heap有两种内存状态free:空闲可用 busy:已被使用;当分配内存块大小<=128页,则以数组存储,其他更大的内存块,则全部放入树结构有序存储。

type mheap struct{
  free [_MaxMHeapList]mSpanList // 以数据存储
  freelarge mTreap                          // 以树型存储

  busy [_MaxMHeapList]mSpanList
  busylarge mSpanList
}
其中_MaxMHeapList = 1 << (20 - _PageShift) = 128

在使用数组代表分配的内存块,则以页数为索引,元素是由多个相同页数的内存块所构成的链表。

type mSpanList struct{  // 内存块链表
    first    *mSpan   // 内存块链表第一个内存块  没有的话记nil
    last    *mSpan   // 内存块链表最后一个内存块 没有的话记nil
}

type mspan struct{
   next  *mspan      // 下一个内存块  没有的话记nil
   prec  *mspan      // 上一个内存块  没有的话记nil
}

当超过128页的内存块集 则使用树来表示,字段freelarge是一个二叉搜索树,以内存块页数和起始地址为排序条件:

每个treapNode代表一个单独的内存块,并且每个treapNode首先以页数排序,对于相同页数的内存块则以对应span的起始地址再排序。而这些内存块集获取是基于最匹配算法来获取的,当获取的spans大小相同时,则选择起始地址最小的那个,即为分配的内存块。


mheap结构

三、内存获取

func (h *mheap) alloc(npage uintptr, spanclass spanClass, large bool, needzero bool) *mspan {
  systemstack(func() {
      s  = h.alloc_m(npage, spanclass, large)
  })
  return s
}

func (h *mheap) alloc_m(npage uintptr, spanclass spanClass, large bool) *mspan{
   s := h.allocSpanLocked(npage, &memstats.heap_inuse)  // 从内存中提取内存块

   if s != nil{
     if large{
      if s.npages < uintptr(len(h.busy)){  // 是否分配的内存页低于128
        h.busy[s.npages].insertBack(s)    // 链表数组
      }  else {
        h.busylarge.insertBack(s)             // 树堆
      }
    }  
  }
   return s 
}

从源码中可看出,当内存块被提取后,放入已使用列表(busy开头的),即代表当前这块内存块已被使用,而真正的核心内容是在allocSpanLocked(npage uintptr, stat *uint64).
当从指定页数开始遍历对应的free数组,在这个过程却没有用页数作为索引直接访问对应的内容,主要的考虑点与条件相符的数组元素可能为空(不管在初始化时还是运行过程中,其对应的链表都可能没有可用的内存块),最佳的做法就是通过继续尝试页数更多的链表,而非去向操作系统申请新内存。换句话说:比如现在指定的15页没有,那么就去16/17页的链表里找;而一旦在链表数组未找到,则继续查找树堆freelarge,若是free里面都没有的话 这个时候是需要向操作系统申请新内存,尽量在已有的内存块中返回容量接近的那个作为分配申请的内存

func (h *mheap) allocSpanLocked(npage uintptr, stat *uint64) *mspan{
   for  i := int(page);  i < len(h.free); i++{ // 从指定页数起,遍历free链表数组
     list = &h.free[i]
     if !list.isEmpty(){ // 有可能当前页数对应的内存span=nil  
        s = list.first      // 获取到最佳的内存
        list.remove(s)  // 剔除free链表数组中的记录
        goto HaveSpan  // 直接进行内存分割
      }  
  }
  // free链表数组没有合适的内存 在freeLarge树堆中查找
  s = h.allocLarge(npage)
  if s == nil{  // freeLarge中并未存在最佳内存 需要向操作系统申请新内存(最少1M,128页)
    if !h.grow(npage){return nil}  // 扩张
    s = h.allocLarge(npage)  // 新申请的内存会放到freeLarge中 获取即可
  }

  HaveSpan: 
     // 分割多余的内存
return s
}

上述的代码也大概给出了获取内存的过程,不过也带出了内存分割的概念

四、内存分割

其实之所以在内存获取过程中会带来内存分割,多半是是因为所返回的内存块大小超出了预期,需要对其进行分割处理,来避免内存的浪费,也由于内存是以页为单位的,在进行分割后优先返回大小合适的内存块,尽量避免碎片化。

func (h *mheap) allocSpanLocked(npage uintptr, stat *uint64) *mspan{
    HaveSpan:
      if s.npages > npage{ // 若是申请到的内存超出预期 需进行分割
           t := (*mspan)(h.spanalloc.alloc()) // 使用新的*mspan对象管理分割下来的内存锁
           t.init(s.base() + npage<<_PageShift, s.npages-npage) // 分割多余的内存块
           s.npages = npage
           // 计算分割的内存块索引 并修改heap.spans反查表内容
           p := (t.base() - h.areana_start) >> _PageShift
           if p > 0 {
              h.spans[p-1] = s  // s_spans 尾部
          }
          h.spans[p] = t          // t_spans头部
          h.spans[p+t.npages-1] = t  // t_spans尾部
         // 分割后的内存块放回heap 并等待内存块合并
         h.freeSpanLocked(t, false, false, s.unusedsince)
      }  
     // 计算索引  并使用span指针来填充heap.spans反查表
     p := (s.base - h.arena_start) >> _PageShift
     for n := uintptr(0); n < npage; n++{
         h.spans[p+n] = s
     }
     return s
}

heap.spans其实就是内存块管理对象mspan的指针


heap.spans

五、内存合并

不论是在内存申请时引起内存分割,还是垃圾回收带来内存释放,只要涉及到将内存放回heap,都会引发合并操作。由于heap.spans反查表可以很方便的访问左右地址相邻内存块,一旦对应的内存块处于自由状态,就可以进行合并操作,循环进行该操作可获得更大的自由空间,来适应更多的内存请求,也减少了内存碎片的存在。

func (h *mheap) freeSpanLocked(s *mspan, acctinuse, acctidle bool, unusedsince int64) {
  s.state = _MSpanFree  // 当前内存空间mspan处于自由状态
  if s.inList(){h.busyList(s.npages).remove(s)} // 需要从busy移除便于后面的操作
  
 p := (s.base() - h.arena_start) >> _PageShift // span索引
 if p > 0{
   before := h.spans[p-1]  // 当前span的左侧相邻的span
   if before != nil && before.state == _MSpanFree{ // 存在并且处于free状态
       // 修改当前span的属性
       s.startAddr = before.startAddr  // 更新当前span起始地址
       s.npages += before.npages      // 更新当前span对应的页数
       
       // 修改heap.spans内容
       p -= before.npages  // 更新当前span的索引
       h.spans[p] = s          // 更新对应的内容

      // 需要将freeList和freeLarge剔除相关记录
      if h.isLargeSpan(before.npages) {
          h.freelarge.removeSpan(before)
      } else{
          h.freeList(before.npages).remove(before)
      }
   }
 }
 // 以上完成将当前span左侧临近的span进行合并了,接下来完成与右侧临近span合并

if (p+s.npages) < uintptr(len(h.spans)) {
     // 获得当前span右侧临近的span
    after := h.spans[p+s.npages]
   
    // 存在并且状态=free
    if after != nil && after.state == _MSpanFree{
      // 修改当前span属性
      s.npages += after.npages

      // 更新heap.spans
      h.spans[p+s.npages-1] = s
 
     // 需要清除free里面原有的span记录(右侧临近的span)
      if h.isLargeSpan(after.npages) {
          h.freelarge.removeSpan(after)
      } else {
          h.freeList(after.npages).remove(after)
      }
   }
}
// 以上完成了当前span与其右侧临近span合并

// 在完成合并后 需要将合并后新的span添加到free里面(包括freeList和freeLarge)
  if h.isLargeSpan(after.npages) {
          h.freelarge.insert(after)
  } else {
          h.freeList(s.npages).insert(after)
  }
}
内存合并

六、内存申请

前面的内容都是建立已有内存基础上进行的,若是现有内存块不足以满足请求时,是需要向操作系统发出申请的,在此过程中主要涉及到两块:
1、每次都申请足够大的内存空间(最少1M,128Pages),是出于性能考虑
2、每次申请足够大的内存空间后若尚未使用,那么操作系统不会立即为其分配物理内存,不用担心造成浪费
具体实现见方法grow

// 默认请求一个大的内存块,一则可以减少操作系统对映射数量的跟踪,
// 二则降低了操作系统映射的开销
// 一般大小是64KB的倍数,并且不能少于1M
func (h *mheap) grow(npage uintptr) bool{       
    npage = round(npage, (64<<10)/_PageSize)
    ask := npage << _PageShift    
    if ask < _HeapAllocChunk {
        ask = _HeapAllocChunk
    }
        
        // 向操作系统申请内容 
    v := h.sysAlloc(ask)
      
       // 
    if v == nil {
        if ask > npage<<_PageShift {  // 防止申请内存块过大
            ask = npage << _PageShift
            v = h.sysAlloc(ask)
        }
        if v == nil {
            print("runtime: out of memory: cannot allocate ", ask, 
"-byte block (", memstats.heap_sys, " in use)\n")
            return false
        }
    }

        // 创建一个mspan进行管理使其处于free以至右侧临近span能够发生合并
    s := (*mspan)(h.spanalloc.alloc())
    s.init(uintptr(v), ask>>_PageShift)

        // 得到当前新建span的对应的索引 并填充heap.spans
    p := (s.base() - h.arena_start) >> _PageShift
    for i := p; i < p+s.npages; i++ {
        h.spans[i] = s
    }

         
    atomic.Store(&s.sweepgen, h.sweepgen)
        // 使得新建span处于“假装在用”状态
    s.state = _MSpanInUse
    h.pagesInUse += uint64(s.npages)

        // 尝试进行合并操作  最终输出合并的结果即为申请的新内存块
    h.freeSpanLocked(s, false, true, 0)
    return true
}

上面也基本罗列出了关于内存请求相关的操作,不过需要说明的一点,完整的工作内存并不仅仅包括arena(一般对应的就是heap堆),还包括bitmap(GC标记)和spans(指针索引)元数据,上面的这些操作都会跟这两个有关联。


完整内存结构

七、其他

func (h *mheap) sysAlloc(n uintptr) unsafe.Pointer{
    if n <= h.arena_end-h.arena_alloc{ // 确保arena还有足够的空间
        p := h.arena_alloc   // 需要用已分配内存当前位置作为分配起始地址
        sysMap(unsafe.Pointer(p), n, h.area_reserved, &memstats.heap_sys) // 系统调用
        h.arena_alloc += n
        
        // 更新heap.spans和bitmap分配内存相关信息
        if h.arena_alloc > h.arena_used{h.setArenaUsed(h.arena_alloc, true)}
       return ussafe.Pointer(p)
    }
 }

func (h *mheap) setArenaUsed(arena_used uintptr, racemap bool){
    h.mapBits(arena_used)
    h.mapSpans(arena_used)

   h.arena_used = arena_used
}

关于golang内存分配 最好能够结合源码进行学习。另外一些涉及到操作系统相关的内容参考下<<深入计算机操作系统>>,便于自己理解相关内容。


有疑问加站长微信联系(非本文作者)

本文来自:简书

感谢作者:神奇的考拉

查看原文:golang对象内存分配

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

4242 次点击  ∙  1 赞  
加入收藏 微博
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传