1、内存管理
以上是程序内存的逻辑分类情况。
我们再来看看一般程序的内存的真实(真实逻辑)图:
2、Go的内存分配
2.1Go的内存分配核心思想
Go是内置运行
时的编程语言(runtime)
,像这种内置运行时的编程语言通常会抛弃传统的内存分配方式,改为自己管理
。这样可以完成类似预分配
、内存池
等操作,以避开系统调用
带来的性能问题
,防止每次分配内存都需要系统调用
。
Go的内存分配的核心思想可以分为以下几点:
(1)每次从操作系统申请一大块儿
的内存,由Go来对这块儿内存做分配,减少系统调用
(2)内存分配算法采用Google的TCMalloc
算法。算法比较复杂,究其原理可自行查阅。其核心思想就是把内存切分的非常的细小,分为多级管理,以降低锁的粒度。
(3)回收对象内存时,并没有
将其真正释放掉
,只是放回预先分配的大块内存
中,以便复用。只有内存闲置过多
的时候,才会尝试归还部分内存给操作系统,降低整体开销
2.2 Go的内存结构
Go在程序启动的时候,会分配一块连续的内存(虚拟内存)。整体如下:
图中span和bitmap的大小会随着heap的改变而改变
arena
arena区域就是我们通常所说的heap。
heap中按照管理和使用两个维度可认为存在两类“东西”:
一类是从管理分配角度,由多个连续的页(page)组成的大块内存:
另一类是从使用角度出发,就是平时咱们所了解的:heap中存在很多"对象":
spans
spans
区域,可以认为是用于上面所说的管理分配arena
(即heap)的区域。
此区域存放了mspan
的指针,mspan
是啥后面会讲。
spans
区域用于表示arena
区中的某一页(page)属于哪个mspan
。
mspan
可以说是go内存管理的最基本单元,但是内存的使用最终还是要落脚到“对象”上。mspan
和对象是什么关系呢?
其实“对象”肯定也放到page中,毕竟page
是内存存储的基本单元。
我们抛开问题不看,先看看一般情况下的对象和内存的分配是如何的:如下图
假如再分配“p4”的时候,是不是内存不足没法分配了?是不是有很多碎片?
这种一般的分配情况会出现内存碎片
的情况,go是如何解决的呢?
可以归结为四个字:按需分配
。go将内存块分为大小不同的67
种,然后再把这67种大内存块,逐个分为小块(
可以近似理解为大小不同的相当于page)称之为span(连续的page),在go语言中就是上文提及的mspan
。
对象分配的时候,根据对象的大小选择大小相近的span
,这样,碎片问题就解决了。
bitmap
bitmap
有好几种:Stack, data, and bss bitmaps,再就是这次要说的heap bitmaps
。
在此bitmap的做作用是标记标记arena
(即heap)中的对象。一是的标记对应地址中是否存在对象
,另外是标记此对象是否被gc标记过
。一个功能一个bit位,所以,heap bitmaps
用两个bit位
。
bitmap区域中的一个byte对应arena区域的四个指针大小的内存的结构如下:
bitmap的地址是由高地址向低地址增长的。
宏观的图为:
bitmap 主要的作用还是服务于GC。
arena中
包含基本的管理单元和程序运行时候生成的对象或实体,这两部分分别被spans
和bitmap
这两块非heap区域的内存所对应着。
逻辑图如下:
spans和bitmap都会根据arena的动态变化而动态调整大小。
2.3内存管理组件
go的内存管理组件主要有:mspan
、mcache
、mcentral
和mheap
(1)mspan
为内存管理的基础单元,直接存储数据的地方。
(2)mcache
:每个运行期的goroutine都会绑定的一个mcache(具体来讲是绑定的GMP并发模型中的P,所以可以无锁分配mspan,后续还会说到),mcache会分配goroutine运行中所需要的内存空间(即mspan)。
(3)mcentral为所有mcache切分好后备的mspan
(4)mheap代表Go程序持有的所有堆空间。还会管理闲置的span,需要时向操作系统申请新内存。
mspan
mspan
是go中内存管理的基本单元,在上文spans
中其实已经做了详细的解说,在此就不在赘述了。
mcache
为了避免多线程申请内存时不断的加锁,goroutine为每个线程分配了span内存块的缓存,这个缓存即是mcache,每个goroutine都会绑定的一个mcache,各个goroutine申请内存时不存在锁竞争的情况。
然后请看下图:
大体上就是上图这个样子了。注意看我们的mcache
在哪儿呢?就在P上!
知道为什么没有锁竞争了吧,因为运行期间一个goroutine只能和一个P关联,而mcache
就在P上,所以,不可能有锁的竞争。
我们再来看看mcache
具体的结构:
mcache中的span链表分为两组
,一组是包含指针类型的对象
,另一组是不包含指针类型的对象
。为什么分开呢?
主要是方便GC
,在进行垃圾回收的时候,对于不包含指针的对象列表无需进一步扫描是否引用其他活跃的对象。
对于 <=32k
的对象,将直接通过mcache
分配。
在此,我觉的有必要说一下go中对象按照的大小维度的分类。
分为三类:
tinny allocations (size < 16 bytes,no pointers)
small allocations (16 bytes < size <= 32k)
large allocations (size > 32k)
前两类:tiny allocations
和small allocations
是直接通过mcache
来分配的。
对于tiny allocations
的分配,有一个微型分配器tiny allocator
来分配,分配的对象都是不包含指针的,例如一些小的字符串和不包含指针的独立的逃逸变量等。
small allocations
的分配,就是mcache
根据对象的大小来找自身存在的大小相匹配mspan
来分配。
当mcach
没有可用空间时,会从mcentral
的 mspans
列表获取一个新的所需大小规格的mspan
。
mcentral
为所有mcache
提供切分好的mspan
。
每个mcentral
保存一种特定类型的全局mspan
列表,包括已分配出去
的和未分配出去
的。
还记得mspan的67种类型吗?有多少种类型的mspan就有多少个mcentral。
每个mcentral都会包含两个mspan的列表:
没有空闲对象或mspan已经被mcache缓存的mspan列表(empty mspanList)
有空闲对象的mspan列表(empty mspanList)
由于mspan是全局的,会被所有的mcache访问,所以会出现并发性问题,因而mcentral会存在一个锁。
单个的mcentral
结构如下:
假如需要分配内存时,mcentral
没有空闲的mspan
列表了,此时需要向mheap
去获取。
mheap
mheap
可以认为是Go程序持有的整个堆空间
,mheap
全局唯一,可以认为是个全局变量。
其结构如下:
mheap包含了除了上文中讲的mcache
之外的一切,mcache
是存在于Go的GMP
调度模型的P
中的。
我们知道,大于32K
的对象被定义为大对象,直接通过mheap
分配。这些大对象
的申请是由mcache
发出的,而mcache在P上,程序运行的时候往往会存在多个P,因此,这个内存
申请是并发的;所以为了保证线程安全
,必须有一个全局锁
。
假如需要分配的内存时,mheap
中也没有了,则向操作系统申请一系列新的页(最小 1MB)。
学习链接:
https://www.jianshu.com/p/2904efc7f1a8