golang内存管理初探

in #cn7 days ago

内存的理解

可以把内存看成一个数组,内存地址可以看成是数组的下标。

CPU执行指令时,通过内存地址(虚拟内存)将物理内存上的数据载入到寄存器执行机器指令。

对于频繁访问的指令会缓存到CPU的多级缓存中。(CPU常规有一级缓存(几十KB),二级缓存,三级缓存等)

寄存器

是一种硬件设备。在计算机中用来存储数据。(可以是指令数据,控制数据,操作数据等等)

寄存器被设计可以被CPU直接访问,通常是嵌入在CPU内部或者与CPU紧密相连的芯片上,速度极快,存储空间比较小(通常字节级别)。

CPU缓存

一种高速缓存,用于存储CPU频繁访问的数据和指令。CPU缓存通常分为L1,L2,L3级别,用来存储高频CPU从内存中访问的数据。

虚拟内存

虚拟内存(Virtual Memory)是计算机系统内存管理的一种技术,它允许操作系统将硬盘空间当作随机访问内存(RAM)来使用。

  1. 用户程序只能通过虚拟内存地址来申请内存空间,OS会将虚拟地址映射到实际地址(MMU)
  2. 虚拟内存本质上是将磁盘 当作最终存储介质,物理内存则作为缓存使用。
  3. 程序可以从虚拟内存中申请很大的内存空间。

虚拟内存的主要目的是

  • 扩展可用内存:通过使用硬盘上的一部分空间作为虚拟内存,计算机可以运行需要比物理内存(RAM)更多的内存的程序。

  • 内存空间隔离:每个程序都认为自己拥有连续的内存空间,而实际上它们可能被分散存储在物理内存和虚拟内存中。这有助于防止程序间的内存冲突。

  • 提高系统性能:操作系统可以根据需要在物理内存和虚拟内存之间移动数据,从而更有效地使用内存资源。

虚拟内存实现

  1. 虚拟内存一般是通过页表来实现(通常每个页4KB)
  2. 磁盘和主存之间的置换也是以页为单位来操作。(缺页中断)
  3. 虚拟地址到物理地址的映射关系由页表(Page Table) 记录。

Q: 多级页表为什么节省内存?

使用多级页表可以节省内存空间。如果使用单一级的页表,需要为整个虚拟地址空间分配一个连续的页表,这会占用大量的内存空间。而多级页表可以将整个页表分解成多个小的页表,只有在需要时才分配和加载

1: 单级页表:
考虑一个32位系统的虚拟地址空间大小为4GB(2^32字节)。
单级页表需要为整个4GB的虚拟地址空间分配一个连续的页表。
单级页表的大小 = 虚拟地址空间大小 / 每页大小 * 页表项大小 = 4GB / 4KB * 4B = 4MB

2: 两级页表:
两级页表将整个页表分解成两级:页目录和页表。
假设页目录大小为4KB(1024个页表项 * 4字节/页表项)。
假设页表大小为4KB(1024个页表项 * 4字节/页表项)。
两级页表的大小 = 页目录大小 + 页表大小 = 4KB + 4KB = 8KB。

image.png

Q: 这里为什么可以只使用部分空间而不用像单级页表那样全部覆盖(每一个 page 都有 PTE)呢?

A: 其实多级页表也是做了全覆盖的,那就是第一级页表,在多级页表中,一级页表是常驻内存的,而且一级页表一条 PTE 就覆盖了 4MB 的内存、整张一级页表覆盖了 4GB 内存,对比单级页表一条 PTE 就映射了 4KB 的内存,效率大大提升。

只有一级页表(与最经常使用的二级页表)常驻主存,其他页表可以在从磁盘调入。


CPU访问内存的过程

image.png

  1. 假设加载程序可执行文件后,执行机器指令MOV ,操作的是虚拟内存地址A,使用寄存器存储数据。
  2. CPU从寄存器将A传递给MMU(内存管理单元),MMU进行虚拟内存地址A到物理内存地址转换B
    2.1 先从TLB(高频映射物理地址 -> 虚拟内存地址条目缓存数据)中查找A是否有映射。
    2.2 若是在TLB中查到不到,则需要通过页表,查找PTE( page table entry) 表中是否存在地址映射。
    image.png
    如果存在,则回写TLB缓存,将内存数据返回CPU寄存器。
    2.3 如果PTE中不存在映射数据,MMU出触发缺页异常, OS 捕获触发缺页中断, 从物理内存索引一个页数据替换PTE中“牺牲页",之后得到A关联映射的B地址。
    image.png
    2.4 如果在物理内存中找不到页数据,则需要将物理内存数据与磁盘数据交换SWAP空间数据

同一个物理地址,在不同的地址空间中有着不同的地址。通过地址转椅得到物理内存地址的PPN (physical page number) 后,结合虚拟内存地址的指定N位bit 来组合得到最后完整的物理地址。


程序的内存布局理解

image.png

  • Linux内核由系统内所有进程共享使用(0xC0000000 - 0xFFFFFFFF)
  • 每个进程有各自独立的私用空间(0-3G空间地址)
  • 内核空间与用户空间隔离以确保内核程序的安全稳定。

image.png

可以使用工具readelf来查看可执行文件的数据段。例如: readelf -S consul-template

There are 13 section headers, starting at offset 0x1c8:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .text             PROGBITS         0000000000401000  00001000
       00000000003fb46c  0000000000000000  AX       0     0     16
  [ 2] .rodata           PROGBITS         00000000007fd000  003fd000
       0000000000177c25  0000000000000000   A       0     0     32
  [ 3] .shstrtab         STRTAB           0000000000000000  00574c40
       000000000000007c  0000000000000000           0     0     1
  [ 4] .typelink         PROGBITS         0000000000974cc0  00574cc0
       0000000000004058  0000000000000000   A       0     0     32
  [ 5] .itablink         PROGBITS         0000000000978d18  00578d18
       0000000000000d18  0000000000000000   A       0     0     8
  [ 6] .gosymtab         PROGBITS         0000000000979a30  00579a30
       0000000000000000  0000000000000000   A       0     0     1
  [ 7] .gopclntab        PROGBITS         0000000000979a40  00579a40
       00000000001f42cc  0000000000000000   A       0     0     32
  [ 8] .noptrdata        PROGBITS         0000000000b6e000  0076e000
       0000000000039021  0000000000000000  WA       0     0     32
  [ 9] .data             PROGBITS         0000000000ba7040  007a7040
       000000000000b6f0  0000000000000000  WA       0     0     32
  [10] .bss              NOBITS           0000000000bb2740  007b2740
       000000000001fda8  0000000000000000  WA       0     0     32
  [11] .noptrbss         NOBITS           0000000000bd2500  007d2500
       0000000000003358  0000000000000000  WA       0     0     32
  [12] .note.go.buildid  NOTE             0000000000400f9c  00000f9c
       0000000000000064  0000000000000000   A       0     0     4


Golang内存管理实现

  • Go内置的运行时runtime实现了自己的管理方式(内存池,预分配 等)

  • runtime内存分配算法TCMALLoc,Thread-Caching Malloc。
    可用堆内存分为2级分配方式管理,每个线程自行维护一个独立内存池。
    进行内存分配申请优先从内存池获取,内存不足再从全局堆内存空间申请空间,避免多个线程对全局堆内存池竞争,类似GMP,毕竟需要SYSCALL申请内存)

Golang程序内存分配图
image.png

  • GO程序动态内存分配申请从arena区域,arena区域是按照每页(8KB大小)分割成多个页。(虚拟内存)
  • mspan是Go程序内存最小颗粒度单位的抽象,一个mspan由多个page组成。会有多个不同规格大小的mspan类型。

当Go程序需要申请分配一块大小为N的内存时候,runtime会根据N的大小分配可用的mspan。在runtime中使用spanclass定义67种不同内存大小的规格。

const (
    _MaxSmallSize   = 32768
    smallSizeDiv    = 8
    smallSizeMax    = 1024
    largeSizeDiv    = 128
    _NumSizeClasses = 68
    _PageShift      = 13
)

var class_to_size = [_NumSizeClasses]uint16{0, 8, 16, 24, 32, 48, 64, 80, 96, 112, 128, 144 ....

image.png

  • bitmap标识arena区域哪些地址保存了对象,并且用4bit标志位表示对象元信息(是否包含指针,GC标记等信息)
    // bitmap stores the pointer/scalar bitmap for the words in
    // this arena. See mbitmap.go for a description. Use the
    // heapBits type to access this.
    bitmap [heapArenaBitmapBytes]byte
  • spans 存放mspan的指针数组,每个指针对应一个page的开始位置(起始页),通过偏移量来确定mspan的大小。spans内的mspan指针可能会包含多个arena内的实际mspan实体
    // spans maps from virtual address page ID within this arena to *mspan.
    // For allocated spans, their pages map to the span itself.
    // For free spans, only the lowest and highest pages map to the span itself.
    // Internal pages map to an arbitrary span.
    // For pages that have never been allocated, spans entries are nil.
    //
    // Modifications are protected by mheap.lock. Reads can be
    // performed without locking, but ONLY from indexes that are
    // known to contain in-use or stack spans. This means there
    // must not be a safe-point between establishing that an
    // address is live and looking it up in the spans array.
    spans [pagesPerArena]*mspan

Go内存分配器

通过三级管理结构: mcache, mcentral, mheap 来高效合理的管理runtime分配的内存。

image.png

  • mcache
  1. Go运行会为每一个逻辑处理器P提供一个本地span缓存(mspan)
  2. 协程需要内存可以先从当前关联的P上的本地span缓存获取。(也就是协程先从mcache申请内存)
  3. mcache 将每种 spanClass 等级的 mspan 各缓存了一个,总数为 2(nocan 维度) * 68(大小维度)= 136
  4. mcache 中还有一个为对象分配器 tiny allocator,用于处理小于 16B 对象的内存分配。

image.png

  • mcentral
  1. mcentral 对象收集所有sizeclass的span
  2. sizeclass相同的span会以链表的形式组织在一起。
  3. mcentral是被所有逻辑处理器P共享的。

image.png

  • mheap
  1. 所有级别的mcentral可以理解是一个数组,统一由mheap进行管理
  2. 另外大对象会直接通过mheap进行分配
  3. mheap是管理内存最核心的单元

image.png


Go内存分配器种对象分类

  • 微小对象 (<= 16B)
    微小对象直接使用mcache的tiny分配器分配。

  • 小对象( 16B < < 32KB)
    根据mcache中相应的规格大小的mspan进行分配。

  • 大对象 (>32KB)
    则直接从mheap上分配。不经过mcache,mcentral。


Go的内存泄漏

内存泄漏指的是在程序运行中,申请的内存没有及时释放内存,导致内存占用持续增长,最终OOM。

  1. 没有及时释放锁,导致内存泄漏(例如多个协程在等待锁未得而进行阻塞的时候,锁不正确释放,会一直等待,如果阻塞协程在IO数据,则会持续占用内存)
  2. 在协程中向一个没有正确初始化或者关闭通道发送或接收数据。(channel的使用姿势不当)
  3. 使用cgo, 没有正确释放C语言分配的内存。

可以通过pprof工具来监控和定位内存泄漏问题。

Coin Marketplace

STEEM 0.20
TRX 0.12
JST 0.028
BTC 64078.16
ETH 3471.05
USDT 1.00
SBD 2.52