golang内存分配

一、分配对象(源码)

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,290评论 6 491
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,107评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,872评论 0 347
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,415评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,453评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,784评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,927评论 3 406
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,691评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,137评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,472评论 2 326
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,622评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,289评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,887评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,741评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,977评论 1 265
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,316评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,490评论 2 348

推荐阅读更多精彩内容

  • 1. 基础知识 1.1、 基本概念、 功能 冯诺伊曼体系结构1、计算机处理的数据和指令一律用二进制数表示2、顺序执...
    yunpiao阅读 5,266评论 1 22
  • GO语言内存管理子系统主要由两部分组成:内存分配器和垃圾回收器(gc)。内存分配器主要解决小对象的分配管理和多线程...
    adrian920阅读 11,184评论 4 6
  • @(嵌入式) [TOC] FreeRtos 提供的几种 heap 管理在源码目录 Source/Portable/...
    orientlu阅读 6,090评论 0 7
  • 概述 我们都知道一个进程是与其他进程共享CPU和内存资源的。正因如此,操作系统需要有一套完善的内存管理机制才能防止...
    SylvanasSun阅读 3,834评论 0 25
  • Day* 【共读日期】【共读主题】【共读笔记】 【共读主要内容】属性阅读法、主动阅读4个逻辑/问题、主动阅读问题与...
    成长路上的碎碎念阅读 189评论 0 0