内存的理解
可以把内存看成一个数组,内存地址可以看成是数组的下标。
CPU执行指令时,通过内存地址(虚拟内存)将物理内存上的数据载入到寄存器执行机器指令。
对于频繁访问的指令会缓存到CPU的多级缓存中。(CPU常规有一级缓存(几十KB),二级缓存,三级缓存等)
寄存器
是一种硬件设备。在计算机中用来存储数据。(可以是指令数据,控制数据,操作数据等等)
寄存器被设计可以被CPU直接访问,通常是嵌入在CPU内部或者与CPU紧密相连的芯片上,速度极快,存储空间比较小(通常字节级别)。
CPU缓存
一种高速缓存,用于存储CPU频繁访问的数据和指令。CPU缓存通常分为L1,L2,L3级别,用来存储高频CPU从内存中访问的数据。
虚拟内存
虚拟内存(Virtual Memory)是计算机系统内存管理的一种技术,它允许操作系统将硬盘空间当作随机访问内存(RAM)来使用。
- 用户程序只能通过虚拟内存地址来申请内存空间,OS会将虚拟地址映射到实际地址(MMU)
- 虚拟内存本质上是将磁盘 当作最终存储介质,物理内存则作为缓存使用。
- 程序可以从虚拟内存中申请很大的内存空间。
虚拟内存的主要目的是:
扩展可用内存:通过使用硬盘上的一部分空间作为虚拟内存,计算机可以运行需要比物理内存(RAM)更多的内存的程序。
内存空间隔离:每个程序都认为自己拥有连续的内存空间,而实际上它们可能被分散存储在物理内存和虚拟内存中。这有助于防止程序间的内存冲突。
提高系统性能:操作系统可以根据需要在物理内存和虚拟内存之间移动数据,从而更有效地使用内存资源。
虚拟内存实现
- 虚拟内存一般是通过页表来实现(通常每个页4KB)
- 磁盘和主存之间的置换也是以页为单位来操作。(缺页中断)
- 虚拟地址到物理地址的映射关系由页表(Page Table) 记录。
Q: 多级页表为什么节省内存?
使用多级页表可以节省内存空间。如果使用单一级的页表,需要为整个虚拟地址空间分配一个连续的页表,这会占用大量的内存空间。而多级页表可以将整个页表分解成多个小的页表,只有在需要时才分配和加载。
1: 单级页表:
考虑一个32位系统的虚拟地址空间大小为4GB(2^32字节)。
单级页表需要为整个4GB的虚拟地址空间分配一个连续的页表。
单级页表的大小 = 虚拟地址空间大小 / 每页大小 * 页表项大小 = 4GB / 4KB * 4B = 4MB
2: 两级页表:
两级页表将整个页表分解成两级:页目录和页表。
假设页目录大小为4KB(1024个页表项 * 4字节/页表项)。
假设页表大小为4KB(1024个页表项 * 4字节/页表项)。
两级页表的大小 = 页目录大小 + 页表大小 = 4KB + 4KB = 8KB。
Q: 这里为什么可以只使用部分空间而不用像单级页表那样全部覆盖(每一个 page 都有 PTE)呢?
A: 其实多级页表也是做了全覆盖的,那就是第一级页表,在多级页表中,一级页表是常驻内存的,而且一级页表一条 PTE 就覆盖了 4MB 的内存、整张一级页表覆盖了 4GB 内存,对比单级页表一条 PTE 就映射了 4KB 的内存,效率大大提升。
只有一级页表(与最经常使用的二级页表)常驻主存,其他页表可以在从磁盘调入。
CPU访问内存的过程
- 假设加载程序可执行文件后,执行机器指令MOV ,操作的是虚拟内存地址A,使用寄存器存储数据。
- CPU从寄存器将A传递给MMU(内存管理单元),MMU进行虚拟内存地址A到物理内存地址转换B。
2.1 先从TLB(高频映射物理地址 -> 虚拟内存地址条目缓存数据)中查找A是否有映射。
2.2 若是在TLB中查到不到,则需要通过页表,查找PTE( page table entry) 表中是否存在地址映射。
如果存在,则回写TLB缓存,将内存数据返回CPU寄存器。
2.3 如果PTE中不存在映射数据,MMU出触发缺页异常, OS 捕获触发缺页中断, 从物理内存索引一个页数据替换PTE中“牺牲页",之后得到A关联映射的B地址。
2.4 如果在物理内存中找不到页数据,则需要将物理内存数据与磁盘数据交换SWAP空间数据。
同一个物理地址,在不同的地址空间中有着不同的地址。通过地址转椅得到物理内存地址的PPN (physical page number) 后,结合虚拟内存地址的指定N位bit 来组合得到最后完整的物理地址。
程序的内存布局理解
- Linux内核由系统内所有进程共享使用(0xC0000000 - 0xFFFFFFFF)
- 每个进程有各自独立的私用空间(0-3G空间地址)
- 内核空间与用户空间隔离以确保内核程序的安全稳定。
可以使用工具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程序内存分配图:
- 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 ....
- 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分配的内存。
- mcache
- Go运行会为每一个逻辑处理器P提供一个本地span缓存(mspan)
- 协程需要内存可以先从当前关联的P上的本地span缓存获取。(也就是协程先从mcache申请内存)
- mcache 将每种 spanClass 等级的 mspan 各缓存了一个,总数为 2(nocan 维度) * 68(大小维度)= 136
- mcache 中还有一个为对象分配器 tiny allocator,用于处理小于 16B 对象的内存分配。
- mcentral
- mcentral 对象收集所有sizeclass的span
- sizeclass相同的span会以链表的形式组织在一起。
- mcentral是被所有逻辑处理器P共享的。
- mheap
- 所有级别的mcentral可以理解是一个数组,统一由mheap进行管理
- 另外大对象会直接通过mheap进行分配
- mheap是管理内存最核心的单元
Go内存分配器种对象分类
微小对象 (<= 16B)
微小对象直接使用mcache的tiny分配器分配。小对象( 16B < < 32KB)
根据mcache中相应的规格大小的mspan进行分配。大对象 (>32KB)
则直接从mheap上分配。不经过mcache,mcentral。
Go的内存泄漏
内存泄漏指的是在程序运行中,申请的内存没有及时释放内存,导致内存占用持续增长,最终OOM。
- 没有及时释放锁,导致内存泄漏(例如多个协程在等待锁未得而进行阻塞的时候,锁不正确释放,会一直等待,如果阻塞协程在IO数据,则会持续占用内存)
- 在协程中向一个没有正确初始化或者关闭通道发送或接收数据。(channel的使用姿势不当)
- 使用cgo, 没有正确释放C语言分配的内存。
可以通过pprof工具来监控和定位内存泄漏问题。