带你领略Go源码的魅力----Go内存原理详解

一、内存分区

代码通过预处理、编译、汇编、连接4步后生成一个可执行程序。python

在 Windows 下,程序是一个普通的可执行文件,如下列出一个二进制可执行文件的基本状况:程序员

经过上图能够得知,在没有运行程序前,也就是说程序没有加载到内存前,可执行程序内部已经分好三段信息,分别为代码区(text)、**数据区(data)未初始化数据区(bss)**3 个部分。算法

有些人直接把data和bss合起来叫作静态区全局区数组

一、1 代码区(text)

存放 CPU 执行的机器指令。一般代码区是可共享的(即另外的执行程序能够调用它),使其可共享的目的是对于频繁被执行的程序,只须要在内存中有一份代码便可。代码区一般是只读的,使其只读的缘由是防止程序意外地修改了它的指令。另外,代码区还规划了局部变量的相关信息。缓存

一、2 全局初始化数据区/静态数据区(data)

该区包含了在程序中明确被初始化的全局变量、已经初始化的静态变量(包括全局静态变量和局部静态变量)和常量数据(如字符串常量)。函数

一、3 未初始化数据区(bss)

存入的是全局未初始化变量和未初始化静态变量。未初始化数据区的数据在程序开始执行以前被内核初始化为 0 或者空(nil)。ui

程序在加载到内存前,代码区和全局区(data和bss)的大小就是固定的,程序运行期间不能改变。spa

而后,运行可执行程序,系统把程序加载到内存,除了根据可执行程序的信息分出代码区(text)、数据区(data)和未初始化数据区(bss)以外,还额外增长了栈区堆区操作系统

一、4 栈区(stack)

栈是一种先进后出的内存结构,由编译器自动分配释放,存放函数的参数值、返回值、局部变量等。线程

在程序运行过程当中实时加载和释放,所以,局部变量的生存周期为申请到释放该段栈空间。

一、5 堆区(heap)

堆是一个大容器,它的容量要远远大于栈,但没有栈那样先进后出的顺序。用于动态内存分配。堆在内存中位于BSS区和栈区之间。

根据语言的不一样,如C语言、C++语言,通常由程序员分配和释放,若程序员不释放,程序结束时由操做系统回收。

Go语言、Java、python等都有垃圾回收机制(GC),用来自动释放内存。

二、 Go Runtime内存分配

Go语言内置运行时(就是Runtime),抛弃了传统的内存分配方式,改成自主管理。这样能够自主地实现更好的内存使用模式,好比内存池、预分配等等。这样,不会每次内存分配都须要进行系统调用。

Golang运行时的内存分配算法主要源自 Google 为 C 语言开发的TCMalloc算法,全称Thread-Caching Malloc

核心思想就是把内存分为多级管理,从而下降锁的粒度。它将可用的堆内存采用二级分配的方式进行管理。

每一个线程都会自行维护一个独立的内存池,进行内存分配时优先从该内存池中分配,当内存池不足时才会向全局内存池申请,以免不一样线程对全局内存池的频繁竞争。

二、1 基本策略

  • 每次从操做系统申请一大块内存,以减小系统调用。
  • 将申请的大块内存按照特定的大小预先的进行切分红小块,构成链表。
  • 为对象分配内存时,只需从大小合适的链表提取一个小块便可。
  • 回收对象内存时,将该小块内存从新归还到原链表,以便复用。
  • 若是闲置内存过多,则尝试归还部份内存给操做系统,下降总体开销。

**注意:**内存分配器只管理内存块,并不关心对象状态,并且不会主动回收,垃圾回收机制在完成清理操做后,触发内存分配器的回收操做

二、2 内存管理单元

分配器将其管理的内存块分为两种:

  • span:由多个连续的页(page [大小:8KB])组成的大块内存。
  • object:将span按照特定大小切分红多个小块,每个小块均可以存储对象。

用途:

span 面向内部管理

object 面向对象分配

//path:Go SDK/src/runtime/malloc.go

_PageShift      = 13
_PageSize = 1 << _PageShift		//8KB
复制代码

在基本策略中讲到,Go在程序启动的时候,会先向操做系统申请一块内存,切成小块后本身进行管理。

申请到的内存块被分配了三个区域,在X64上分别是512MB,16GB,512GB大小。

**注意:**这时还只是一段虚拟的地址空间,并不会真正地分配内存

  • arena区域

    就是所谓的堆区,Go动态分配的内存都是在这个区域,它把内存分割成8KB大小的页,一些页组合起来称为mspan。

    //path:Go SDK/src/runtime/mheap.go
    
    type mspan struct {
    	next           *mspan    	// 双向链表中 指向下一个
    	prev           *mspan    	// 双向链表中 指向前一个
    	startAddr      uintptr   	// 起始序号
    	npages         uintptr   	// 管理的页数
    	manualFreeList gclinkptr 	// 待分配的 object 链表
         nelems 		   uintptr 		// 块个数,表示有多少个块可供分配
         allocCount     uint16		// 已分配块的个数
    	...
    }
    复制代码
  • bitmap区域

    标识arena区域哪些地址保存了对象,而且用4bit标志位表示对象是否包含指针、GC标记信息。

  • spans区域

    存放mspan的指针,每一个指针对应一页,因此spans区域的大小就是512GB/8KB*8B=512MB。

    除以8KB是计算arena区域的页数,而最后乘以8是计算spans区域全部指针的大小。

二、3 内存管理组件

内存分配由内存分配器完成。分配器由3种组件构成:

  • cache

    每一个运行期工做线程都会绑定一个cache,用于无锁 object 的分配

  • central

    为全部cache提供切分好的后备span资源

  • heap

    管理闲置span,须要时向操做系统申请内存

二、三、1 cache

cache:每一个工做线程都会绑定一个mcache,本地缓存可用的mspan资源。

这样就能够直接给Go Routine分配,由于不存在多个Go Routine竞争的状况,因此不会消耗锁资源。

mcache 的结构体定义:

//path:Go SDK/src/runtime/mcache.go

_NumSizeClasses = 67					//67
numSpanClasses = _NumSizeClasses << 1	//134

type mcache struct {
	alloc [numSpanClasses]*mspan		//以numSpanClasses 为索引管理多个用于分配的 span
}
复制代码

mcache用Span Classes做为索引管理多个用于分配的mspan,它包含全部规格的mspan。

它是 _NumSizeClasses 的2倍,也就是67*2=134,为何有一个两倍的关系。

为了加速以后内存回收的速度,数组里一半的mspan中分配的对象不包含指针,另外一半则包含指针。对于无指针对象的mspan在进行垃圾回收的时候无需进一步扫描它是否引用了其余活跃的对象。

二、三、2 central

central:为全部mcache提供切分好的mspan资源。

每一个central保存一种特定大小的全局mspan列表,包括已分配出去的和未分配出去的。

每一个mcentral对应一种mspan,而mspan的种类致使它分割的object大小不一样。

//path:Go SDK/src/runtime/mcentral.go

type mcentral struct {
	lock      mutex     	// 互斥锁
	sizeclass int32     	// 规格
	nonempty  mSpanList 	// 尚有空闲object的mspan链表
	empty     mSpanList 	// 没有空闲object的mspan链表,或者是已被mcache取走的msapn链表
	nmalloc   uint64    	// 已累计分配的对象个数
}
复制代码

二、三、3 heap

heap:表明Go程序持有的全部堆空间,Go程序使用一个mheap的全局对象_mheap来管理堆内存。

当mcentral没有空闲的mspan时,会向mheap申请。而mheap没有资源时,会向操做系统申请新内存。mheap主要用于大对象的内存分配,以及管理未切割的mspan,用于给mcentral切割成小对象。

同时咱们也看到,mheap中含有全部规格的mcentral,因此,当一个mcache从mcentral申请mspan时,只须要在独立的mcentral中使用锁,并不会影响申请其余规格的mspan。

//path:Go SDK/src/runtime/mheap.go
type mheap struct {
	lock        mutex
	spans       []*mspan // spans: 指向mspans区域,用于映射mspan和page的关系
	bitmap      uintptr  // 指向bitmap首地址,bitmap是从高地址向低地址增加的
	arena_start uintptr  // 指示arena区首地址
	arena_used  uintptr  // 指示arena区已使用地址位置
	arena_end   uintptr  // 指示arena区末地址
	central [numSpanClasses]struct {
		mcentral mcentral
		pad      [sys.CacheLineSize-unsafe.Sizeof(mcentral{})%sys.CacheLineSize]byte
	}					//每一个 central 对应一种 sizeclass
}
复制代码

二、4 分配流程

  • 计算待分配对象的规格(size_class)
  • 从cache.alloc数组中找到规格相同的span
  • 从span.manualFreeList链表提取可用object
  • 若是span.manualFreeList为空,从central获取新的span
  • 若是central.nonempty为空,从heap.free/freelarge获取,并切分红object链表
  • 若是heap没有大小合适的span,向操做系统申请新的内存

二、5 释放流程

  • 将标记为可回收的object交还给所属的span.freelist
  • 该span被放回central,能够提供cache从新获取
  • 若是span以所有回收object,将其交还给heap,以便从新分切复用
  • 按期扫描heap里闲置的span,释放其占用的内存

注意:以上流程不包含大对象,它直接从heap分配和释放

二、6 总结

Go语言的内存分配很是复杂,它的一个原则就是能复用的必定要复用。

  • Go在程序启动时,会向操做系统申请一大块内存,以后自行管理。
  • Go内存管理的基本单元是mspan,它由若干个页组成,每种mspan能够分配特定大小的object。
  • mcache, mcentral, mheap是Go内存管理的三大组件,层层递进。mcache管理线程在本地缓存的mspan;mcentral管理全局的mspan供全部线程使用;mheap管理Go的全部动态分配内存。
  • 通常小对象经过mspan分配内存;大对象则直接由mheap分配内存。

接下来是Go语言曾经的一大黑点:垃圾回收(GC)。能够关注咱们的公开课,法师会带着你们一块儿深刻了解Go语言的GC发展和机制,扫一扫二维码,观看公开课的直播。

相关文章
相关标签/搜索