系统相关
首页 > 系统相关> > golang--内存管理器

golang--内存管理器

作者:互联网

一.前言

笔者在经过了前期基础学习后,用go语言来实现自己面临的业务问题已经不再是问题,所以拥有了另一方面的求知欲--go语言自身的各种包,各种机制是如何实现的,本章主要在探究go语言的内存分配器,希望能用本文讲清楚go语言内存分配器的机制,帮助大家更好地理解go语言的运行机制。

二.简介

不同于c语言使用malloc和free来主动管理内存,golang让程序员避免了这一切繁杂的操作,它通过escape analysis来分配内存,通过garbage collection(gc)来回收内存,本文主要介绍内存分配部分的工作。

三.详细解释

3.1 golang内存分配时机

程序有两种内存,一种是堆栈(stack),一种是堆(heap),所有的堆内数据都被GC管理。

我们要明白什么时候程序会分配内存,在某些语言中是程序员主动申请的,在go语言中则依赖escape analysis,越多的值在堆栈,程序运行越快(存取速度比堆要快,仅次于直接位于CPU中的寄存器,以下是内存分配的一些时机

1. goloang只会把函数中确定不在函数结束后使用的变量放到堆栈,否则就会放到堆:一个值可能在构造该值的函数之后被引用-->变量上传

package main


func main(){
	n:=answer()
	println(*n/2)
}

func answer() *int{
	x:=42
	return &x
}

使用命令 go build -gcflags="-m -l"得到结果

./main.go:10:2: moved to heap: x

2.编译器确定值太大而无法放入堆栈

3.编译器在编译的时候无法得知这个值的具体大小

ps:将变量下传,变量还会留在堆栈中

type Reader interface{
   Read(p []byte) (n int,err error)
}
//better than 
type Reader interface{
   Read(n int) (b []byte,err error)
}
//因为从上面传参数下去用的是堆栈,从下面往上传,则会escape到堆,导致程序更慢

3.2 golang内存分配方式

3.2.1 TCMalloc

学习go语言的内存分配方式之前,我们先来看看另一个内存分配器-->TCMalloc,全称Thread-Caching Malloc

TCMalloc有两个重要组成部分:线程内存(thread cache)页堆(page heap)

3.2.1.1 线程内存

每一个内存页都被分为多个固定分配大小规格的空闲列表(free list) 用于减少碎片化。这样每一个线程都可以获得一个用于无锁分配小对象的缓存,这样可以让并行程序分配小对象(<=32KB)非常高效。

Thread Cache (Each Thread gets this Thread Local Thread Cache)

如图所示,第一行就是长度为8字节的内存块,在thread cache内最大的为256字节的内存块

3.2.1.2 页堆

TCMalloc 管理的堆由一组页组成(page一般大小为4kb),一组连续的页面被表示为 span。当分配的对象大于 32KB,将使用页堆(Page Heap)进行内存分配,分配对象时,大的对象直接分配 Span,小的对象从 Span 中分配。

Page Heap (for span management)

当没有足够的空间分配小对象则会到页堆获取内存。如果页堆页没有足够的内存,则页堆会向操作系统申请更多的内存。

3.2.1.3 内存分配器

将基于 Page 的对象分配,和Page本身的管理串联

每种规格的对象,都从不同的 Span 进行分配;每种规则的对象都有一个独立的内存分配单元:CentralCache。在一个CentralCache 内存,我们用链表把所有 Span 组织起来,每次需要分配时就找一个 Span 从中分配一个 Object;当没有空闲的 Span 时,就从 PageHeap 申请 Span。

3.2.1.3 总结

最终我们得到结构图如下:

TCMalloc针对不同的对象分配采用了不同的形式

每个线程都一个线程局部的 ThreadCache,按照不同的规格,维护了对象的链表;如果ThreadCache 的对象不够了,就从 CentralCache 进行批量分配;如果 CentralCache 依然没有,就从PageHeap申请Span;如果 PageHeap没有合适的 Page,就只能从操作系统申请了。

在释放内存的时候,ThreadCache依然遵循批量释放的策略,对象积累到一定程度就释放给 CentralCache;CentralCache发现一个 Span的内存完全释放了,就可以把这个 Span 归还给 PageHeap;PageHeap发现一批连续的Page都释放了,就可以归还给操作系统。

由此,TCMalloc的核心思路即:

把内存分为多级管理,从而降低锁的粒度。它将可用的堆内存采用二级分配的方式进行管理:每个线程都会自行维护一个独立的内存池,进行内存分配时优先从该内存池中分配,当内存池不足时才会向全局内存池申请,以避免不同线程对全局内存池的频繁竞争。

3.2.2 Go内存分配器结构

3.2.2.1 初始化

Go在程序启动的时候,会先向操作系统申请一块内存(注意这时还只是一段虚拟的地址空间,并不会真正地分配内存),切成小块后自己进行管理。

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

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

bitmap区域标识arena区域哪些地址保存了对象,并且用4bit标志位表示对象是否包含指针、GC标记信息。bitmap中一个byte大小的内存对应arena区域中4个指针大小(指针大小为 8B )的内存,所以bitmap区域的大小是512GB/(4*8B)=16GB

spans区域存放mspan(也就是一些arena分割的页组合起来的内存管理基本单元,后文会再讲)的指针,每个指针对应一页,所以spans区域的大小就是512GB/8KB*8B=512MB。除以8KB是计算arena区域的页数,而最后乘以8是计算spans区域所有指针的大小。创建mspan的时候,按页填充对应的spans区域,在回收object时,根据地址很容易就能找到它所属的mspan

spans区域存放mspan(也就是一些arena分割的页组合起来的内存管理基本单元,后文会再讲)的指针,每个指针对应一页,所以spans区域的大小就是512GB/8KB*8B=512MB。除以8KB是计算arena区域的页数,而最后乘以8是计算spans区域所有指针的大小。创建mspan的时候,按页填充对应的spans区域,在回收object时,根据地址很容易就能找到它所属的mspan

go初始化的时候会将内存页分为如下67个不同大小的内存块,最大到32kb

Size Classes in Go

3.2.2.2 结构及流程总览

Go的内存分配器在分配对象时,根据对象的大小,分成三类:小对象(小于等于16B)、一般对象(大于16B,小于等于32KB)、大对象(大于32KB)。

大体上的分配流程:

1.32KB 的对象,直接从mheap上分配;

2.<=16B 的对象使用mcache的tiny分配器分配;

3.(16B,32KB] 的对象,首先计算对象的规格大小,然后使用mcache中相应规格大小的mspan分配;

4.如果mcache没有相应规格大小的mspan,则向mcentral申请

5.如果mcentral没有相应规格大小的mspan,则向mheap申请

6.如果mheap中也没有合适大小的mspan,则向操作系统申请

3.2.2.3 自底向上名词解释

3.2.2.3.1 内存管理单元

mspan:Go中内存管理的基本单元,是由一片连续的8KB的页组成的大块内存。是一个包含起始地址、mspan规格、页的数量等内容的双端链表,mspan由一组连续的页组成,按照一定大小划分成object

结构图

Illustrative Representation of a mspan in Go memory allocator

3.2.2.3.2 内存管理元件

mcache:Go 像 TCMalloc 一样为每一个 逻辑处理器(P)(Logical Processors) 提供一个本地线程缓存(Local Thread Cache)称作 mcache,所以如果 Goroutine 需要内存可以直接从 mcache 中获取,由于在同一时间只有一个 Goroutine 运行在 逻辑处理器(P)(Logical Processors) 上,所以中间不需要任何锁的参与。

对于每一种大小规格都有两个类型:

  1. scan -- 包含指针的对象。

  2. noscan -- 不包含指针的对象。

采用这种方法的好处之一就是进行垃圾回收时 noscan 对象无需进一步扫描是否引用其他活跃的对象。

(<=16B 的对象使用mcache的tiny分配器分配)

结构体

//path: /usr/local/go/src/runtime/mcache.go

type mcache struct {
    alloc [numSpanClasses]*mspan
}

numSpanClasses = _NumSizeClasses << 1

结构图

Illustrative Representation of a Relationship between P, mcache, and mspan in Go.

central(mcentral):为所有mcache提供切分好的mspan资源。每个central保存一种特定大小的全局mspan列表,包括已分配出去的和未分配出去的。 每个mcentral对应一种mspan,而mspan的种类导致它分割的object大小不同。当工作线程的mcache中没有合适(也就是特定大小的)的mspan时就会从mcentral获取。mcentral被所有的工作线程共同享有,存在多个Goroutine竞争的情况,因此会消耗锁资源。

//path: /usr/local/go/src/runtime/mcentral.go

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

结构图

Illustrative Representation of a mcentral

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

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

同时我们也看到,mheap中含有所有规格的mcentral,所以,当一个mcachemcentral申请mspan时,只需要在独立的mcentral中使用锁,并不会影响申请其他规格的mspan

//path: /usr/local/go/src/runtime/mheap.go

type mheap struct {
    lock mutex
    // spans: 指向mspans区域,用于映射mspan和page的关系
    spans []*mspan 
    // 指向bitmap首地址,bitmap是从高地址向低地址增长的
    bitmap uintptr 

    // 指示arena区首地址
    arena_start uintptr 
    // 指示arena区已使用地址位置
    arena_used  uintptr 
    // 指示arena区末地址
    arena_end   uintptr 

    central [67*2]struct {
        mcentral mcentral
        pad [sys.CacheLineSize - unsafe.Sizeof(mcentral{})%sys.CacheLineSize]byte
    }
}

结构图

Illustrative Representation of a mheap.

arena:golang中所有堆区的统称,以x64为例子就是512GB的虚拟地址空间。

四.之后目标

1.go语言的垃圾回收

2.进程调度,线程调度,协程调度

3.虚拟内存

五.参考学习

https://www.youtube.com/watch?v=ZMZpH4yT7M0

https://www.linuxzen.com/go-memory-allocator-visual-guide.html

https://zhuanlan.zhihu.com/p/29216091

https://zhuanlan.zhihu.com/p/59125443

标签:mspan,--,mcentral,golang,对象,内存,go,管理器,分配
来源: https://blog.csdn.net/hello_bravo_/article/details/119078657