关于golang:Golang内存管理详解

47次阅读

共计 8682 个字符,预计需要花费 22 分钟才能阅读完成。

根底

存储金字塔

  • CPU 寄存器
  • CPU Cache:三级 Cache 别离是 L1、L2、L3,L1 最快,L3 最慢
  • 内存
  • 硬盘等辅助存储设备
  • 鼠标等外接设备

从上至下的访问速度越来越慢,拜访工夫越来越长。

虚拟内存

拜访内存,理论拜访的是虚拟内存,虚拟内存通过页表查看,以后要拜访的虚拟内存地址,是否曾经加载到了物理内存。如果曾经在物理内存,则取物理内存数据,如果没有对应的物理内存,则从磁盘加载数据到物理内存,并把物理内存地址和虚拟内存地址更新到页表。

物理内存就是磁盘存储缓存层,在没有虚拟内存的时代,物理内存对所有过程是共享的,多过程同时拜访同一个物理内存会存在并发问题。而引入虚拟内存后,每个过程都有各自的虚拟内存,内存的并发拜访问题的粒度从多过程级别,能够升高到多线程级别。

栈和堆

代码中应用的内存地址都是虚拟内存地址,而不是理论的物理内存地址。栈和堆只是虚拟内存上 2 块不同性能的内存区域:

  • 栈在高地址,从高地址向低地址增长
  • 堆在低地址,从低地址向高地址增长

栈和堆相比有这么几个益处:

  • 栈的内存治理简略,调配比堆上快。
  • 栈的内存不须要回收,而堆须要进行回收,无论是被动 free,还是被动的垃圾回收,这都须要破费额定的 CPU。
  • 栈上的内存有更好的局部性,堆上内存拜访就不那么敌对了,CPU 拜访的 2 块数据可能在不同的页上,CPU 拜访数据的工夫可能就下来了。

内存分区

Golang 内存分区:代码区、数据区、堆区、栈区

// 低地址  ——----------------------------------------------------------------》高地址
// 代码区   |   数据区(初始化数据区,未初始化数据区,常量区)|   堆区  |  栈区(函数信息,外部变量)// 函数地址(0x7c7620):代码区。是一个低地址地位,计算机指令
// 全局变量(0xd03250):初始化数据区,如果初始化了:初始化数据;未初始化:未初始化数据
// 局部变量(0xc0000120b0):栈区,高地址
// 堆区:一个很大的空间,在应用时,开拓内存空间,完结时,开释内存空间。// 栈区:用来存储程序执行过程中函数外部定义的信息和局部变量值。

最内层函数后进先出,最内层函数先执行后,开释内存,向下层传递后果。函数 return 返回值将函数执行的后果保留下来,返回给调用者。

变量

局部变量

  • 在 C 语言中写在 {} 中或者函数中或者函数的形参, 就是局部变量
  • Go 语言中的局部变量和 C 语言一样

全局变量

  • 在 C 语言中写在函数里面的就是全局变量
  • Go 语言中的全局变量和 C 语言一样

局部变量和全局变量的作用域

  • 在 C 语言中局部变量的作用域是从定义的那一行开始, 直到遇到 } 完结或者遇到 return 为止
  • Go 语言中局部变量的作用域和 C 语言一样
  • 在 C 语言中全局变量的作用域是从定义的那一行开始, 直到文件开端为止
  • Go 语言中的全局变量, 只有定义了, 在定义之前和定义之后都能够应用

局部变量和全局变量的生命周期

  • 在 C 语言中局部变量, 只有执行了才会调配存储空间, 只有来到作用域就会主动开释, C 语言的局部变量存储在栈区
  • Go 语言局部变量的生命周期和 C 语言一样
  • 在 C 语言中全局变量, 只有程序一启动就会调配存储空间, 只有程序敞开才会开释存储空间, C 语言的全局变量存储在动态区(数据区)
  • Go 语言全局变量的生命周期和 C 语言一样

局部变量和全局变量的留神点

  • 在 C 语言中雷同的作用域内, 不能呈现同名的局部变量
  • Go 语言和 C 语言一样, 雷同干的作用域内, 不能呈现同名的局部变量
package main
import "fmt"
func main() {
    var num int; // 局部变量
    //var num int; // 报错, 不能呈现同名局部变量
}
  • 在 C 语言中雷同的作用域内, 能够呈现同名的全局变量
  • 在 Go 语言中雷同的作用域内, 不能呈现同名的全局变量
    例:
package main
import "fmt"
var value int // 全局变量
//var value int // 报错, 不能呈现同名全局变量
func main() {}

非凡点

  • 在 C 语言中 局部变量 没有初始化存储的是垃圾数据, 在 Go 语言中局部变量没有初始化, 会默认初始化为 0
  • 在 C 语言中 全局变量 没有初始化存储的是 0, Go 语言和 C 语言一样
  • 在 Go 语言中, 如果定义了一个 局部变量, 然而没有应用这个局部变量, 编译会报错
  • 在 Go 语言中, 如果定义了一个 全局变量, 然而没有应用这个全局变量, 编译不会报错

留神点

  • 雷同的作用域内, 无论是全局变量还是局部变量, 都不能呈现同名的变量
  • 变量来到作用域就不能应用
  • 局部变量如果没有应用, 编译会报错, 全局变量如果没有应用, 编译不会报错
  • := 只能用于局部变量, 不能用于全局变量
  • := 如果用于同时定义多个变量, 会有进化赋值景象,如果通过:= 定义多个变量, 然而多个变量中有的变量曾经在后面定义过了, 那么只会对没有定义过的变量执行:=, 而定义过的变量只执行 = 操作

堆内存治理

内存调配 Malloc : memory allocator

当咱们说内存治理的时候,次要是指堆内存的治理,因为栈的内存治理不须要程序去操心。

当发现内存申请的时候,堆内存就会从未分配内存宰割出一个小内存块 (block),而后用链表把所有内存块连接起来。须要一些信息形容每个内存块的根本信息,比方大小(size)、是否应用中(used) 和下一个内存块的地址(next),内存块理论数据存储在 data 中。

一个内存块蕴含了 3 类信息:元数据、用户数据和对齐字段。

开释内存本质是把应用的内存块从链表中取出来,而后标记为未应用,当分配内存块的时候,能够从未应用内存块中优先查找大小相近的内存块,如果找不到,再从未调配的内存中分配内存。

TCMalloc(Thread Cache Malloc)

TCMalloc是 Google 开发的内存分配器,Golang 应用了相似的算法进行内存调配。

同一过程下的所有线程共享雷同的内存空间,它们申请内存时须要加锁,如果不加锁就存在同一块内存被 2 个线程同时拜访的问题。

TCMalloc 的做法是什么呢?为每个线程预调配一块缓存,线程申请小内存时,能够从缓存分配内存,这样有 2 个益处:

  1. 为线程预调配缓存须要进行 1 次零碎调用,后续线程申请小内存时间接从缓存调配,都是在用户态执行的,没有了零碎调用,缩短了内存总体的调配和开释工夫,这是疾速分配内存的第二个档次。
  2. 多个线程同时申请小内存时,从各自的缓存调配,拜访的是不同的地址空间,从而无需加锁,把内存并发拜访的粒度进一步升高了,这是疾速分配内存的第三个档次。

基本原理

page

  • 操作系统对内存治理以页为单位
  • TCMalloc 里的 Page 大小与操作系统里的大小并不一定相等,而是倍数关系
  • x64 下 Page 大小是 8KB。

Span

  • 一组间断的 Page 被称为 Span,比方能够有 2 个页大小的 Span,也能够有 16 页大小的 Span
  • Span 比 Page 高一个层级,是为了方便管理肯定大小的内存区域
  • Span 是 TCMalloc 内存治理的根本单位

ThreadCache

  • ThreadCache 是每个线程各自的 Cache
  • 一个 Cache 蕴含多个闲暇内存块链表,每个链表连贯的都是内存块,同一个链表上内存块的大小是雷同的
  • 这样能够依据申请的内存大小,疾速从适合的链表抉择闲暇内存块。因为每个线程有本人的 ThreadCache
  • ThreadCache 拜访是无锁的

CentralCache

  • CentralCache 是所有线程共享的缓存,也是保留的闲暇内存块链表,链表的数量与 ThreadCache 中链表数量雷同
  • 当 ThreadCache 的内存块有余时,能够从 CentralCache 获取内存块;当 ThreadCache 内存块过多时,能够放回 CentralCache。
  • 因为 CentralCache 是共享的,所以它的拜访是要加锁的。

PageHeap

  • PageHeap 是对堆内存的形象,PageHeap 存的也是若干链表,链表保留的是 Span。
  • 当 CentralCache 的内存不足时,会从 PageHeap 获取闲暇的内存 Span,而后把 1 个 Span 拆成若干内存块,增加到对应大小的链表中并分配内存;
  • 当 CentralCache 的内存过多时,会把闲暇的内存块放回 PageHeap 中。
  • 能够有是 1 页 Page 的 Span 链表,2 页 Page 的 Span 链表等,最初是 large span set,这个是用来保留中大对象的。
  • PageHeap 也是要加锁的。

TCMalloc 对象大小的定义:

  • 小对象大小:0~256KB
  • 中对象大小:257KB~1MB
  • 大对象大小:>1MB

对象调配流程:

  • 小对象的调配流程:

    • ThreadCache -> CentralCache -> HeapPage
    • 大部分时候,ThreadCache 缓存都是足够的,不须要去拜访 CentralCache 和 HeapPage,无零碎调用配合无锁调配,调配效率是十分高的。
  • 中对象调配流程:间接在 PageHeap 中抉择适当的大小即可,128 Page 的 Span 所保留的最大内存就是 1MB。
  • 大对象调配流程:从 large span set 抉择适合数量的页面组成 span,用来存储数据。

Go 内存构造

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

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

arena

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

bitmap

bitmap 区域 标识 arena 区域哪些地址保留了对象 ,并且用4bit 标记位示意对象是否蕴含指针、GC标记信息。

bitmap中一个 byte 大小的内存对应 arena 区域中 4 个指针大小(指针大小为 8B)的内存,所以 bitmap 区域的大小是512GB/(4*8B)=16GB

spans

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

Go 内存治理

GO 比 TCMalloc 还多了 2 件货色:逃逸剖析和垃圾回收

基本概念

page

与 TCMalloc 中的 Page 雷同

span

  • 与 TCMalloc 中的 Span 雷同,代码中为 mspan
  • Span 是内存治理的根本单位

mcache

mcache 是提供给 P 的本地内存池。

mcache 与 TCMalloc 中的 ThreadCache 相似,mcache 保留的是各种大小的 Span,并按 Span class 分类,小对象间接从 mcache 分配内存,它起到了缓存的作用,并且能够无锁拜访。

不同点:

  • TCMalloc 中是每个线程 1 个 ThreadCache,Go 中是每个 P 领有 1 个 mcache
  • 因为在 Go 程序中,以后最多有 GOMAXPROCS 个线程在运行,所以最多须要 GOMAXPROCS 个 mcache 就能够保障各线程对 mcache 的无锁拜访,线程的运行又是与 P 绑定的,把 mcache 交给 P 刚刚好。

mcentral

mcentral 与 TCMalloc 中的 CentralCache 相似,是所有线程共享的缓存,须要加锁拜访。它按 Span 级别对 Span 分类,而后串联成链表,当 mcache 的某个级别 Span 的内存被调配光时,它会向 mcentral 申请 1 个以后级别的 Span。

不同点:

  • CentralCache 是每个级别的 Span 有 1 个链表
  • mcentral 是每个级别的 Span 有 2 个链表

mheap

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

  • mheap 与 TCMalloc 中的 PageHeap 相似,它是堆内存的形象,把从 OS 申请出的内存页组织成 Span,并保存起来。
  • 当 mcentral 的 Span 不够用时会向 mheap 申请内存,而 mheap 的 Span 不够用时会向 OS 申请内存。
  • mheap 向 OS 的内存申请是按页来的,而后把申请来的内存页生成 Span 组织起来,同样也是须要加锁拜访的。

不同点:

  • mheap 把 Span 组织成了树结构,而不是链表,并且还是 2 棵树
  • mheap 把 Span 调配到 heapArena 进行治理,它蕴含地址映射和 span 是否蕴含指针等位图,这样做的次要起因是为了更高效的利用内存:调配、回收和再利用。

GO 内存大小转化

  1. object size:代码里简称 size,指申请内存的对象大小。
  2. size class:代码里简称 class,它是 size 的级别,相当于把 size 归类到肯定大小的区间段

    • size[1,8]属于 size class 1
    • size(8,16]属于 size class 2
    • size(16,32]属于 size class 3
    • size(32,48]属于 size class 4
  3. span class:指 span 的级别,但 span class 的大小与 span 的大小并没有反比关系。span class 次要用来和 size class 做对应,1 个 size class 对应 2 个 span class,2 个 span class 的 span 大小雷同,只是性能不同,1 个用来寄存蕴含指针的对象,一个用来寄存不蕴含指针的对象,不蕴含指针对象的 Span 就无需 GC 扫描了。
  4. num of page:代码里简称 npage,代表 Page 的数量,其实就是 Span 蕴含的页数,用来分配内存。
class  1      2      3      4      5      6  ···   63      64      65      66

bytes  8      16     32     48     64     80 ···  24576   27264   28672   32768

Go 内存调配

内存调配由内存分配器实现。分配器由 3 种组件形成:mcache, mcentral, mheap

内存分类

  • 当要调配大于 32K 的对象时,从 mheap 调配。
  • 当要调配的对象小于等于 32K 大于 16B 时,从 P 上的 mcache 调配,如果 mcache 没有内存,则从 mcentral 获取,如果 mcentral 也没有,则向 mheap 申请,如果 mheap 也没有,则从操作系统申请内存。
  • 当要调配的对象小于等于 16B 时,从 mcache 上的微型分配器上调配。

大小对象

  • 小对象:小对象是在 mcache 中调配的

    • Tiny 对象:大小在 1~16Byte 之间并且不蕴含指针的对象
    • 其余小对象:16Byte~32KB
  • 大对象:大于 32KB,间接从 mheap 调配

小对象内存调配

  • size class 数量:_NumSizeClasses=67
  • span class 数量:numSpanClasses = _NumSizeClasses * 2 = 134
  • 也就是 mcache 最多有 134 个 span
1. 为对象寻找 span:
  1. 计算对象所需内存大小 size
  2. 依据 size 到 size class 映射,计算出所需的 size class
  3. 依据 size class 和对象是否蕴含指针计算出 span class
  4. 获取该 span class 指向的 span
  5. 举例:24Byte 对象属于 size class 3,对应的 span class 为 7
2. 从 span 调配对象空间
  • Span 能够按对象大小切成很多份:以 size class 3 对应的 span 为例,span 大小是 8KB,每个对象理论所占空间为 32Byte,这个 span 就被分成了 256 块。
  • 随着内存的调配,span 中的对象内存块,有些被占用,有些未被占用,当分配内存时,只有疾速找到第一个可用的绿色块,并计算出内存地址即可。
  • 当 span 内的所有内存块都被占用时,没有残余空间持续调配对象,mcache 会向 mcentral 申请 1 个 span,mcache 拿到 span 后持续调配对象。
3. mcache 向 mcentral 申请 span

mcentral 和 mcache 一样,都是 0~133 这 134 个 span class 级别,但每个级别都保留了 2 个 span list,即 2 个 span 链表:

  1. nonempty:这个链表里的 span,所有 span 都至多有 1 个闲暇的对象空间。这些 span 是 mcache 开释 span 时退出到该链表的。
  2. empty:这个链表里的 span,所有的 span 都不确定外面是否有闲暇的对象空间。当一个 span 交给 mcache 的时候,就会退出到该链表

mcache 向 mcentral 申请 span 时,mcentral 会先从 nonempty 搜寻满足条件的 span,如果没有找到再从 emtpy 搜寻满足条件的 span,而后把找到的 span 交给 mcache。

4. mheap 的 span 治理

mheap 里保留了两棵二叉排序树,按 span 的 page 数量进行排序:

  1. free:free 中保留的 span 是闲暇并且非垃圾回收的 span。
  2. scav:scav 中保留的是闲暇并且曾经垃圾回收的 span。

如果是垃圾回收导致的 span 开释,span 会被退出到 scav,否则退出到 free,比方刚从 OS 申请的的内存也组成的 Span。

mheap 中还有 arenas(动态分配的堆区),由一组 heapArena 组成,每一个 heapArena 都蕴含了间断的 pagesPerArena 个 span,这个次要是为 mheap 治理 span 和垃圾回收服务。arenas 自身是一个全局变量,它外面的数据,也都是从 OS 间接申请来的内存,并不在 mheap 所治理的那局部内存以内。

5. mcentral 向 mheap 申请 span

当 mcentral 向 mcache 提供 span 时,如果 empty 里也没有符合条件的 span,mcentral 会向 mheap 申请 span。

此时,mcentral 须要向 mheap 提供须要的内存页数和 span class 级别,而后它优先从 free 中搜寻可用的 span。如果没有找到,会从 scav 中搜寻可用的 span。如果还没有找到,它会向 OS 申请内存,再从新搜寻 2 棵树,必然能找到 span。

如果找到的 span 比须要的 span 大,则把 span 进行宰割成 2 个 span,其中 1 个刚好是需要大小,把剩下的 span 再退出到 free 中去,而后设置须要的 span 的根本信息,而后交给 mcentral。

6. mheap 向 OS 申请内存

当 mheap 没有足够的内存时,mheap 会向 OS 申请内存,把申请的内存页保留为 span,而后把 span 插入到 free 树。此时,mcentral 须要向 mheap 提供须要的内存页数和 span class 级别,而后它优先从 free 中搜寻可用的 span。如果没有找到,会从 scav 中搜寻可用的 span。如果还没有找到,它会向 OS 申请内存,再从新搜寻 2 棵树,必然能找到 span。

如果找到的 span 比须要的 span 大,则把 span 进行宰割成 2 个 span,其中 1 个刚好是需要大小,把剩下的 span 再退出到 free 中去,而后设置须要的 span 的根本信息,而后交给 mcentral。

大对象内存调配

当要调配大于 32K 的对象时,从 mheap 调配。

大对象的调配比小对象省事多了,99% 的流程与 mcentral 向 mheap 申请内存的雷同,所以不反复介绍了。不同的一点在于 mheap 会记录一点大对象的统计信息,详情见 mheap.alloc_m()。

垃圾回收和内存开释

  • 垃圾回收收集不再应用的 span,调用 mspan.scavenge()把 span 开释还给 OS(并非真开释,只是通知 OS 这片内存的信息无用了,如果你需要的话,发出去好了)
  • 而后交给 mheap,mheap 对 span 进行 span 的合并,把合并后的 span 退出 scav 树中
  • 期待再分配内存时,由 mheap 进行内存再调配

栈内存

每个 goroutine 都有本人的栈,栈的初始大小是 2KB,100 万的 goroutine 会占用 2G,但 goroutine 的栈会在 2KB 不够用时主动扩容,当扩容为 4KB 的时候,百万 goroutine 会占用 4GB。

应用程序的内存会分成堆区(Heap)和栈区(Stack)两个局部,程序在运行期间能够被动从堆区申请内存空间,这些内存由内存分配器调配并由垃圾收集器负责回收

栈区的内存由编译器主动进行调配和开释,栈区中存储着函数的参数以及局部变量,它们会随着函数的创立而创立,函数的返回而销毁

go 语言编译器会主动决定把一个变量放在栈还是放在堆,编译器会做逃逸剖析(escape analysis),当发现变量的作用域没有跑出函数范畴,就能够在栈上,反之则必须调配在堆。

总结

Go 内存调配治理的策略有如下几点:

  • Go 在程序启动时,会向操作系统申请一大块内存,由 mheap 构造全局治理。
  • Go 内存治理的根本单元是 mspan,每种mspan 能够调配特定大小的object
  • mcachemcentralmheapGo 内存治理的三大组件:

    • mcache治理线程在本地缓存的mspan(无锁)
    • mcentral治理全局的 mspan 供所有线程应用(有锁)
    • mheap治理 Go 的所有动静分配内存。(有锁)
  • Tiny 对象(0~16B 且无指针),个别小对象通过 mcache 分配内存(16B~32K);大对象则间接由 mheap 分配内存(大于 32K)。

Reference

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

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

https://blog.haohtml.com/arch…

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

https://blog.csdn.net/kevin_t…

正文完
 0