Golang内存分布--从源码到分析

一、内存分配器
    程序中的数据和变量都会被分配到程序所在的虚拟内存中,内存才能空间包含两个重要区域: 栈区(Stack)和堆区(Heap)。

    函数调用的参数、返回值以及局部变量大都会被分配到栈上、这部分内存会有编译器进行管理。
    不同编程语言使用不同方法管理堆区的内存,c++、等编程语言会由使用者主动申请和释放内存;
    Go和Java等编程语言会由使用者和编译器共同管理。
    堆中的对象有内存分配器分配并由垃圾收集器回收。
        栈区速度>>堆区速度,堆区分配繁琐,栈区只需要push和pop

    Golang的内存分配器借鉴了tcmalloc的思想,尽量减少在多线程模型下,锁的竞争开销,开提高内存分配的效率。


##  锁的必要性:
        在内存空间中被划分为了多个内存单元格,当两个线程来申请内存空间时,如果不进行加锁,可能会给两个线程划分到一个内存空间,那整个内存空间到底谁使用呢?这就会起冲突。
        所以需要引用锁的概念,在第一个线程申请这块空间后,给这块内存空间上锁,给其他线程分配其他内存空间就能解决这个问题。


二、TCMalloc
    tcmalloc,其实就是thread cache malloc的缩写,线程缓存内存分配机制。
    
    1.tcmalloc的实现:
        tcmalloc内存分配分为了ThreadCache (线程缓存层)、CentralCache (中心缓存)、PageHeap (内存页堆)三个层次。
        
        在一个进程申请到的内存空间上,用PageHeap的概念分成了多个 span ,每一个span都是一个用来管理内存的逻辑概念,我把它理解为单元格单位;
        上一层时CentralCache,里面分为了从8byte到32kb的不同大小 (size class)的一堆链表,每个链表管理着一堆的span。在这一层仍然是被线程共享内存的,所以在这一层上还是需要加锁的;
        再上一层的ThreadCache,每个线程都会独享一个ThreadCache空间,每个空间都会缓存着一些空闲链表,每个线程申请内存空间是只会在ThreadCache中查找。
        因为ThreadCache是独享内存的,所以在这一层上不需要锁竞争,减少了开销。

        ThreadCache是每一个线程的缓存,分配是不需要加锁,速度比较快。ThreadCache中对于每一个size class维护一个单独的FreeList,缓存还没有分配的空闲对象  (占位)。
        CentralCache也同样为每一个size class维护一个Freelist,但是这一层是所有线程公用的,分配时需要加锁。
        CentralCache中内存不足时,会从PageHeap中申请。CentralCache从PageHeap中申请的内存,可能来自于PageHeap的缓存,也可能时PageHeap从操作系统中申请的新的内存。
        PageHeap内部,对于128个Page以内的span,会都用一个链表来缓存,超过了128个page的span,则存储于一个有序的set。

        每一层都可能会出现内存不够用的情况,这时候就会向下一层申请内存空间

    2.tcmalloc的优势:
        · 减少锁的争用
        · 事先一次性向操作系统申请了一大片的内存空间,再由内存分配器进行管理,提高性能。

##  为什么事先申请空间再分配会性能会更好?
        每一次向操作系统申请内存,需要经过系统调用,需要上下文切换,比较耗费性能;
        先申请大量内存再分配,只需要指针的操作就能分配出去。


三、Golang的内存分配器
    Golang语言的内存分配器借鉴了tcmalloc的思想;
    Go语言的内存分配器包含内存管理单元、线程缓存、中心缓存和页堆几个重要组件;
    下面会详细介绍它们对应的数据结构 mspan、 mcache、 mcentral、 mheap以及它们再内存分配器中的作用以及实现。

    每一个处理器 P都会分配一个线程缓存mcache用于处理微对象和小对象的分配,它们会持有内存管理单元mspan;
    每个size class的 mspan都会管理特定大小的对象,当mspan中不存在空闲对象时,它们会从mcentral中获取新的mspan;
    mcentral属于全局的堆结构体 mheap,mheap会从操作系统中申请内存
    mheap中包含多个heap arena,每一个heap arena是一段连续的内存,对应的是操作系统的页page


四、内存管理组件 (mspan)
    mspan是一个内存管理单元的基本单元

    每个mspan都对应一个大小等级(67个),小对象类型的堆对象会根据其大小,分配到相应设定好大小等级的mspan上分配内存
    微对象:(0,16B)     先使用微型分配器,在依次尝试mcache、mcentral、mheap分配内存;
    小对象:[16B,32KB]  依次尝试使用mcache、mcentral、mheap分配内存;
    大对象:(32KB,+∞)   直接再mheap上分配内存。

    mspan: 是Go语言内存管理的基本单元,该结构体包含next和prev两个字段,分别指向了前一个和后一个的mspan;
    串联后形成一个双向链表,mspanlist存储双向链表的头节点和尾节点并再mcache和mcentral中使用。

    span--跨度
        可以理解为一段连续的内存空间
        每个mspan都管理 npages个大小为8KB的页,这里的页不是操作系统中的内存页,它们是操作系统中内存页的整数倍;
        mspan使用 startAddr、npages、freeindex、allocBits、gcmarkBits、allocCache 字段来管理内存页的分配和回收;

        当mspan管理的内存不足时,会以页为单位向mheap申请内存,更改 startAddr和npages ;
        当线程向mspan申请内存时,会使用startAddr和allocCache快速查找空闲对象;
        如果能在内存中找到空闲的内存单元会直接返回,当内存中不包含空闲对象时,mcache会调用 mcache.refill更新mspan以满足内存需求。

    spanclass--跨度类
        用于表示span的对象大小和span类型
        Go语言的一共有67种spanclass,每一个spanclass都标shi存储特定大小的对象并且包含特定数量的页数以及对象;
        所有的数据都会被虚线计算好并存储在runtime.class_to_size和runtime.class_to_allocnpages中。
        
        spanclass所能表示span中对象大小从8btye到32768(32kb)一共67种span
        能标示span的大小、spen的页数、span中对象大小、对象个数等信息
        spanclass为5的span:
            对象大小上限为48byte,页数为1页。
            所以span大小为8*1024byte,最大对象数量为170个,尾部浪费为32byte;
            最小存储对象为33btye,最多会浪费31.52%

        spanclass除了存储类别的id之外,还会存储一个noscan标记位,该标记位表示对象是否包含指针,垃圾回收会对包含指针的mpan进行扫描

        spanclass是一个uint整数,前7位存储class id,最后一位存储noscan标识

    type mspan struct {
	    next *mspan         // list中的下一个span,没有则为nil
	    prev *mspan         // list中的上一个span,没有则为nil
        
	    startAddr uintptr   // 确定span管理页的偏移量
	    npages    uintptr   // 确定span管理的页数
        freeindex uintptr   // 页中空闲对象的索引

        allocBits  *gcBits  // 标记内存的占用情况
	    gcmarkBits *gcBits  // 标记内存的回收情况
        allocCache uint64   // allocBits的补码,用于快速查找内存中未被使用的内存

        spanclass   spanClass   //  用于表示span的类型
        ...
    }


五、线程缓存 (mcache)
    mcache是Go语言中的线程缓存,它会与线程上的处理器(GMP-P)绑定;
    每一个线程分配一个mcache用于处理微对象和小对象的内存分配;
    因为是每个线程独有的,所以不需要加锁。

    mcache刚被初始化的时候是不包含mspan的,只有当用户申请内存时才从上一级组件mcentral获取性的mspan满足内存分配的需求

    mcache会持有tiny相关字段用于微对象内存分配
    mcache会持有mspan用于小对象的内存分配

    alloc:
        用于分配内存的mspan数组;
        数组大小位span类型总数的两倍(67*2),即每种span类型都有两个mspan,一个表示noscan,一个是scan。
        为了提高GC扫描性能,对于noscan没必要去扫描,而scan则需要GC进行扫描。
    mcache在刚刚被初始化是alloc中mspan是空的占位符emptymspan。当mcache中mspan的空闲内存不足时,会向mcentral组件请求获取mspan。

    · 微分配器 TinyAllocator
        线程缓存中还包括几个分配微对象的字段,tiny、tinyoffset、tinyAllocs三个字段组成了微对象分配器,专门管理16byte以下的对象。
        微分配器只会用于分配非指针类型的内存。

    type mcache struct {
        tiny       uintptr      // 指向堆中的一片内存,即当前偏移量
	    tinyoffset uintptr      // 下一个空闲内存所在的偏移量
        tinyAllocs uintptr      //会记录内存分配器中分配的对象个数
        ...
    }   


六、中心缓存 (mcentral)
    mcentral时内存分配器的中心缓存,与线程缓存不同,访问中心缓存中的mspan需要使用互斥锁

    每个mcentral都会管理某个spanclass的内存管理单元;
    他会持有两个runtime.spanSet,分别存储包含空闲对象和不包含空闲对象的mspan。

    partial:有空闲空间的span列表
    full:没有空闲空间的span列表

    与mcache不同,mcentral是公共资源,会有多个线程的mcache向mcentral申请mspan,依次访问mcentral中的mspan时需要使用互斥锁。
    mcache会通过mcentra的cacheSpan方法获取mspan,并更新mcache的alloc字段中相应span class对应的mspan

    从mcentral中申请资源的时候,会优先向有空闲空间的span列表申请mspan,再向无空闲空间的span列表申请mspan,如果获取失败,会再从mheap中申请mspan。

##  为什么再向mheap中申请span之前还要先尝试向full列表申请呢?
        因为再向mheap申请之前会进行一次GC,会把一些已经满的但是不使用的mspan的空间回收掉,可能会出现一些空的mspan

    如果申请到了mspan,会更新mspan中的 allocBits和allocCache字段。
    runtime.refill会为线程缓存获取一个指定span class的mspan;
    被替换的(old)mspan不能包含空闲的内存空间,而获取的mspan中需要至少包含一个空闲对象。  (旧的一定要非常旧,新的不一定要非常新)

    type mcentral struct {
	spanclass spanClass
    partial [2]spanSet 
	full    [2]spanSet 
    }


七、页堆 (mheap)
    内存分配的核心组件,包含mcentral和heapArena,堆上所有mspan都是mheap结构分配来的

    allspans:  已经分配的所有mspan
    arenas:    heapArena数组,用于管理一个个内存块
    central:   mcentral数组,用于管理对应spanClass的mspan

    heapArena:
        Go 1.11后采用稀疏内存管理。堆区的内存可以不连续,将堆区内存分成一个个内存块(arena),通过heapArena管理
    在mheap中维护一个heapArena数组,记录所有内存块:
        bitmap:标记heapArena内存中没饿过地址空间的使用情况
        spans:记录page id映射到的mspan
        pageInUse、pageMarks:标记状态为mSpanInUse或在GC扫描中被标记的page,用于加速内存回收
        zeroedBase:标记此arena中尚未使用的第一个page的首地址

    mcentral从mheap中申请新的mspan时:
        1. 从heap的pages中获取内存,并生成mspan
        2. heap中无法获取到mspan时,需要对堆增长,调用sysAlloc从操作系统中申请的内存
        3. 从已经保留的heapArena中获取内存
        4. 无法获取到合适的内存时,会向操作系统申请,并进行heapArena初始化,将heapArena加入到arena列表中


八、内存分配
    堆上所有的对象都会通过调用 rentime.newobject 函数分配内存;
    该函数会调用 runtime.mallocgc 分配指定大小的内存空间,这也是程序向堆上申请内存空间的必经函数。

    func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
        mp := acquirem()
        mp.mallocing = 1

        c := gomcache()
        var x unsafe.Pointer
        noscan := typ == nil || typ.ptrdata == 0
        if size <= maxSmallSize {
                if noscan && size < maxTinySize {
                        // 微对象分配
                } else {
                        // 小对象分配
                }
        } else {
                // 大对象分配
        }

        publicationBarrier()
        mp.mallocing = 0
        releasem(mp)

        return x
    }
    
    上述代码使用runtime.gomcache 获取线程缓存并判断申请内存的类型是否为指针。
    而且还可以看出runtime.mallocgc 会根据对象的大小执行不同的分配逻辑,在前面也提到过根据对象大小将他们分成微对象、小对象、大对象

    · 微对象:(0,16byte) 先使用微型分配器,在依次尝试mcache、mcentral、mheap分配内存;
        会使用微型分配器提高微对象分配性能,我们主要使用它来分配较小的字符串和逃逸的临时变量;
        微分配器可以将多个较小的内存分配请求何如同一个内存块中,只有当内存块中的所有对象都需要被回收时,整片内存才可能被回收。

        微分配器管理的对象不可以是指针类型,管理多个对象的内存块大小 maxTinySize是可以调整的,默认情况是16字节。
        maxTinySize越大,组合多个对象的可能性就越高,内存浪费就越严重;
        maxTinySize越小,内存浪费越少,但是提供了越少的组合可能;
        最好的方案是8的倍数

        还记得微分配器的结构吗?
            tiny       uintptr      // 指向推中的一片内存,即当前偏移量
	        tinyoffset uintptr      // 下一个空闲内存所在的偏移量
        假设tiny标记的16byte的微分配器已经被分配了12byte,tinyoffset标记了12byte之后;
        如果下一个待分配对象小于16-12=4byte,他会直接使用这个内存块的剩余部分,减少内存碎片;
        不过该内存块只有所有对象都被标记为垃圾时,才会回收。
        代码:
            mcache中的tiny字段指向了maxTinySize大小的块;
            如果当前块中还包含大小合适的空闲内存,运行时会通过这tiny和tinyoffset直接获取并返回这块内存
        func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
            ...
            if size <= maxSmallSize {
                if noscan && size < maxTinySize {
                        off := c.tinyoffset
                        if off+size <= maxTinySize && c.tiny != 0 {
                                x = unsafe.Pointer(c.tiny + off)
                                c.tinyoffset = off + size
                                c.local_tinyallocs++
                                releasem(mp)
                                return x
                        }
                        ...
                }
                ...
            }   
            ...
        }

        
        如果内存块的空闲内存不够时,会先从mcache中找到跨度对应的mspan,,调用runtime.nextFreeFast获取空闲的内存;
        当不存在空闲内存时,调用runtime.mcache.nextFree从mcentral或mheap中获取可分配的内存块
        代码:
            func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
                ...
                if size <= maxSmallSize {
                    if noscan && size < maxTinySize {
                        ...
                        span := c.alloc[tinySpanClass]
                        v := nextFreeFast(span)
                        if v == 0 {
                                v, _, _ = c.nextFree(tinySpanClass)
                        }
                        x = unsafe.Pointer(v)
                        (*[2]uint64)(x)[0] = 0
                        (*[2]uint64)(x)[1] = 0
                        if size < c.tinyoffset || c.tiny == 0 {
                                c.tiny = uintptr(x)
                                c.tinyoffset = size
                        }
                        size = maxTinySize
                    }
                    ...
                }
                ...
                return x
            }
        获取新的空闲内存块之后,上述代码会清空空闲内存中的数据、更新构成微对象分配器的几个字段并返回空闲内存


    · 小对象:
        大小为16byte到32kb的对象以及所有小于16byte的指针对象,小对象的分配可以分为三个步骤:
                1. 确定分配对象的大小以及spanclass
                2. 从mcache、mcentral、mheap中获取mspan并从mspan中找到空闲的内存空间
                3. 调用 runtime.memclrNoHeapPointers 清空空闲内存中的所有数据
        
        确定待分配对象的大小以及spanclass需要使用预先计算好的size_to_class8、size_to_class128、class_to_divmagic字典,这些常量能帮助快速获取对应的值并构建。
            func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
                ...
                if size <= maxSmallSize {
                    ...
                    } else {
                            var sizeclass uint8
                            if size <= smallSizeMax-8 {
                                    sizeclass = size_to_class8[(size+smallSizeDiv-1)/smallSizeDiv]
                            } else {
                                    sizeclass = size_to_class128[(size-smallSizeMax+largeSizeDiv-1)/largeSizeDiv]
                            }
                            size = uintptr(class_to_size[sizeclass])
                            spc := makeSpanClass(sizeclass, noscan)
                            span := c.alloc[spc]
                            v := nextFreeFast(span)
                            if v == 0 {
                                    v, span, _ = c.nextFree(spc)
                            }
                            x = unsafe.Pointer(v)
                            if needzero && span.needzero != 0 {
                                    memclrNoHeapPointers(unsafe.Pointer(v), size)
                            }
                        }
                } else {
                        ...
                }
                ...
                return x
            }

        重点分析两个方法的实现原理,它们分别是 runtime.nextFreeFast和runtime.mcache.nextFree,这两个方法会帮助我们获取空闲的内存空间。
        runtime.nextFreeFast会利用mspan中的allocCache字段 (占用位图的补码),快速找到该字段为1的位数,
        找到了空闲对象后,我们就可以更新mspan的allocCache、freeindex等字段并返回该片内存
        代码:    
            func nextFreeFast(s *mspan) gclinkptr {
                theBit := sys.Ctz64(s.allocCache)
                if theBit < 64 {
                    result := s.freeindex + uintptr(theBit)
                    if result < s.nelems {
                        freeidx := result + 1
                        if freeidx%64 == 0 && freeidx != s.nelems {
                                return 0
                        }
                        s.allocCache >>= uint(theBit + 1)
                        s.freeindex = freeidx
                        s.allocCount++
                        return gclinkptr(result*s.elemsize + s.base())
                    }
                }
                return 0
            }
        
        如果通过runtime.nextFreeFast没有找到空闲的内存,会通过runtime.mcache.nextFree找到新的内存管理单元
        代码:
            func (c *mcache) nextFree(spc spanClass) (v gclinkptr, s *mspan, shouldhelpgc bool) {
                    s = c.alloc[spc]
                freeIndex := s.nextFreeIndex()
                if freeIndex == s.nelems {
                        c.refill(spc)
                        s = c.alloc[spc]
                        freeIndex = s.nextFreeIndex()
                }

                v = gclinkptr(freeIndex*s.elemsize + s.base())
                s.allocCount++
                return
            }
        
        如果在mcache中没有找到可用的mspan,会通过前面介绍的mcache.refill使用mcentral中的mspan替换已经不存在可用对象的数据;
        该方法会调用新结构体的mspan.nextFreeIndex获取空闲的内存并返回


    · 大对象:
        运行时对于大于32KB的大对象会单独处理,我们不会从mcache或者mcentral中获取mspan而是直接调用mcache.allocLarge:
            func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
                ...
                if size <= maxSmallSize {
                    ...
                } else {
                        var s *mspan
                        span = c.allocLarge(size, needzero, noscan)
                        span.freeindex = 1
                        span.allocCount = 1
                        x = unsafe.Pointer(span.base())
                        size = span.elemsize
                }

                publicationBarrier()
                mp.mallocing = 0
                releasem(mp)

                return x
            }       

        mcache.allocLarge会计算分配给该对象所需要的页数,它按照8KB的背书在堆上申请内存。
        申请内存时会创建一个spanclass为0的特殊spancalss并调用mheap.alloc分配一个管理对应内存的mspan
        代码:
            func (c *mcache) allocLarge(size uintptr, needzero bool, noscan bool) *mspan {
                    npages := size >> _PageShift
                    if size&_PageMask != 0 {
                            npages++
                    }
                    ...
                    s := mheap_.alloc(npages, spc, needzero)
                    mheap_.central[spc].mcentral.fullSwept(mheap_.sweepgen).push(s)
                    s.limit = s.base() + size
                    heapBitsForAddr(s.base()).initSpan(s)
                    return s
            }


九、内存分配总结:
    Go的内存分配器在给对象分配内存时,根据对象的大小,分成三类微对象(0,16btye]、小对象(16btye,32KB]、大对象(32KB,+∞)。
    大体上的分配流程:
        1. 大对象直接从mheap上分配;
        2. 微对象使用mcache上的tiny分配器分配;
        3. 小对象首先计算对象的规格大小,然后使用mcache中相应规格大小的mspan分配;
        4.如果mcache没有相应规格大小的mspan,指向mcentral申请;
        4.如果mcentral没有相应规格大小的mspan,指向mheap申请;
        4.如果mheap没有相应规格大小的mspan,指向操作系统申请;


十、逃逸分析:
    逃逸分析是一种静态分析,在编译阶段执行。
    每当函数中申请新对象,编译器会根据该对象是否被函数外部引用来分析,决定变量应该在栈上非,还是在堆上分配。

    · 两个不变性:
        1. 指向栈对象的指针不能存在于堆中
        2. 指向栈对象的指针不能在栈对象收回后存活
    
    · 主要策略:
        1. 如果函数外部没有引用,则优先放到栈中;
        2. 如果函数外部存在引用,则必定放到堆中;
    
    · 场景:
        1. 函数返回局部变量,则会引起内存逃逸
        2. 动态类型引起的逃逸
        3. 栈空间不足导致逃逸
        4. 闭包函数中没有定义变量i的,而是引用了他所在函数f中的变量i,变量i发生逃逸


十一、如何利用逃逸分析提升性能
    传值 VS 传指针:
        传值会拷贝整个对象,而传指针只会拷贝指针地址,指向的对象是同一个。
        传指针可以减少值的拷贝,但是导致内存逃逸到堆中,增加垃圾回收(GC)的负担。
        在对象频繁创建和删除的场景下,传递指针导致的GC开销可能会严重影响性能。

        一般情况下,对于需要修改原对象值,或占用内存比较大的结构体,选择传指针。
        对于只读的占用内存比较小的结构体,直接传值能够获得更好的性能。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值