golang笔记——Go堆内存管理

前言

本文主要记录个人学习Golang堆内存管理,涉及到的相关内容,算是对个人所学知识点的梳理与总结。从非常宏观的角度看,Go的堆内存管理就是下图这个样子


学习内存管理,肯定首先需要了解内存管理的基本知识,我会按照 内存管理基础知识-->TCMalloc-->Go堆内存管理基础概念-->Go堆内存分配流程,这样的顺序来逐步梳理相关知识。

内存管理基础知识

1. 存储器与内存

在计算机的组成结构中有一个很重要的部分是存储器。它是用来存储程序和数据的部件。对于计算机来说,有了存储器,才有记忆功能,才能保证正常工作。存储器的种类很多。按其用途可分为主存储器(也称为内存储器,简称内存)和辅助存储器(也称为外存储器)。

外存储器主要是指除计算机内存及CPU缓存以外的储存器,此类储存器一般断电后仍然能保存数据。常见的外存储器有硬盘、软盘、光盘、U盘等。

内存一般采用半导体存储单元,包括随机存储器(RAM),只读存储器(ROM),以及高速缓存(CACHE)。

  • 只读存储器 ROM(Read Only Memory)
    只能读出,一般不能写入,即使机器停电,这些数据也不会丢失。一般用于存放计算机的基本程序和数据,如BIOS ROM。

  • 随机存储器 RAM(Random Access Memory)
    既可以从中读取数据,也可以写入数据。当机器电源关闭时,存于其中的数据就会丢失。
    RAM分为两种:动态存储芯片(DRAM)和静态存储芯片(SRAM)。

    1. DRAM:DRAM结构较简单且集成度高,通常用于制造内存条中的存储芯片。
    2. SRAM:SRAM速度快且不需要刷新操作,但集成度差和功耗较大,通常用于制造容量小但效率高的CPU缓存。
  • 高速缓存 Cache
    高速缓冲存储器是存在于主存与CPU之间的一级存储器, 由静态存储芯片(SRAM)组成,容量比较小但速度比主存高得多, 接近于CPU的速度。由于从1980年开始CPU和内存速率差距在不断拉大,为了弥补这2个硬件之间的速率差异,所以在CPU跟内存之间增加了比内存更快的Cache,Cache是内存数据的缓存,可以降低CPU访问内存的时间。

    三级Cache分别是L1、L2、L3,它们的速率是三个不同的层级,L1速率最快,与CPU速率最接近,是RAM速率的100倍,L2速率就降到了RAM的25倍,L3的速率更靠近RAM的速率。

寄存器是CPU内部用来存放数据的一些小型存储区域,用来暂时存放参与运算的数据和运算结果。

那么当CPU要去读取来自远程网络服务器上的磁盘文件时,就是由CPU直接和远程服务器磁盘交互吗?事实当然不是这样的。由于CPU的执行速率远远高于外部存储的读写速率,所以当CPU去读取磁盘中数据时,通常会先查看离自己最近的寄存器是否有缓存对应的数据,如果存在想要的数据就会直接获取。而寄存器的读写速率十分接近CPU,将数据缓存在寄存其中可以极大地提升执行效率,避免低效的磁盘读写降低性能。

由于计算机的存储体系中,存储量越大越低廉的存储设备往往读写越慢,存储量越小越昂贵的存储设备往往读写越快。而为了存储更多的数据,大量数据往往存储在读写慢的存储设备上。为了让CPU在执行读写操作时,执行效率尽可能地不被读写慢的存储设备影响,于是下图中的存储器层次结构便孕育而生了。

存储器层次结构

存储器层次结构的主要思想,就是让读写更快的存储设备作为读写慢但容量更大的存储器的高速缓存,让CPU每次优先访问上层读写更快的设备,尽量减少与低效存储设备的读写交互,以保证计算机的整体性能。

2. 虚拟内存

2.1 为什么使用虚拟内存

计算机对于内存真正的载体是物理内存条,这个是实打实的物理硬件容量,所以在操作系统中定义这部份的容量叫物理内存(主存)。物理内存的布局实际上就是一个内存大数组,如图所示。

每一个元素都会对应一个地址,称之为物理内存地址。那么CPU在运算的过程中,如果需要从内存中取1个字节的数据,就需要基于这个数据的物理内存地址去运算即可,而且物理内存地址是连续的,可以根据一个基准地址进行偏移来取得相应的一块连续内存数据。

一个操作系统是不可能只运行一个程序的,当N个程序共同使用同一个物理内存时,就会存在以下问题:

1. 内存资源是稀缺的,每个进程为了保证自己能够运行,会为自己申请额外大的内存,导致空闲内存被浪费
2. 物理内存对所有进程是共享的,多进程同时访问同一个物理内存会存在并发问题

为了解决以上问题,操作系统便引入了虚拟内存。通过虚拟内存作为物理内存和进程之间的中间层,让进程通过虚拟内存来访问物理内存。引入了虚拟内存后的操作系统如图所示。


用户程序(进程)只能使用虚拟的内存地址来获取数据,进程会通过页表中的虚拟内存地址查看Memory Map,判断当前要访问的虚拟内存地址,是否已经加载到了物理内存。如果已经在物理内存,则取物理内存数据,如果没有对应的物理内存,则从磁盘加载数据到物理内存,并把物理内存地址和虚拟内存地址更新到页表。

引入虚拟内存后,每个进程都有各自的虚拟内存,内存的并发访问问题的粒度从多进程级别,可以降低到多线程级别。从程序的角度来看,它觉得自己独享了一整块内存,且不用考虑访问冲突的问题。系统会将虚拟地址翻译成物理地址,从内存上加载数据。但如果仅仅把虚拟内存直接理解为地址的映射关系,那就是过于低估虚拟内存的作用了。

虚拟内存的目的是为了解决以下几件事:
(1)物理内存无法被最大化利用。
(2)程序逻辑内存空间使用独立。
(3)内存不够,继续虚拟磁盘空间。

2.2 读时共享,写时复制

其中针对(1)的最大化,虚拟内存还实现了“读时共享,写时复制”的机制,可以在物理层同一个字节的内存地址被多个虚拟内存空间映射,表现方式下图所示。


上图所示 如果一个进程需要进行写操作,则这个内存将会被复制一份,成为当前进程的独享内存。如果是读操作,可能多个进程访问的物理空间是相同的空间

如果一个内存几乎大量都是被读取的,则可能会多个进程共享同一块物理内存,但是他们的各自虚拟内存是不同的。当然这个共享并不是永久的,当其中有一个进程对这个内存发生写,就会复制一份,执行写操作的进程就会将虚拟内存地址映射到新的物理内存地址上。

2.3 虚拟内存映射磁盘空间

对于第(3)点,是虚拟内存为了最大化利用物理内存,如果进程使用的内存足够大,则导致物理内存短暂的供不应求,那么虚拟内存也会“开疆拓土”从磁盘(硬盘)上虚拟出一定量的空间,挂在虚拟地址上,而且这个动作进程本身是不知道的,因为进程只能够看见自己的虚拟内存空间,如下图所示。



综上可见虚拟内存的重要性,不仅提高了利用率而且整条内存调度的链路完全是对用户态物理内存透明,用户可以安心的使用自身进程独立的虚拟内存空间进行开发。

3. 页、页表、页表条目


  • 页是1次内存读取的大小,操作系统中用来描述内存大小的一个单位名称。一个页的含义是大小为4K(1024*4=4096字节,可以配置,不同操作系统不一样)的内存空间。操作系统对虚拟内存空间是按照这个单位来管理的。
  • 页表
    页表实际上就是页表条目(PTE)的集合,就是基于PTE的一个数组,页表的大小是以页(4K)为单位的。

    虚拟内存的实现方式,大多数都是通过页表来实现的。操作系统虚拟内存空间分成一页一页的来管理,每页的大小为 4K(当然这是可以配置的,不同操作系统不一样)。4K 算是通过实践折中出来的通用值,太小了会出现频繁的置换,太大了又浪费内存。
  • 页表条目(PTE)
    页表条目(PTE)是页表中的一个元素,PTE是真正起到虚拟内存寻址作用的元素。PTE的内部结构如下图所示。

    PTE是由一个有效位和一个包含物理页号或者磁盘地址组成,有效位表示当前虚拟页是否已经被缓存在主内存中(或者CPU的高速缓存Cache中)。
    (1)有效位为1,表示虚拟页已经被缓存在内存(或者CPU高速缓存TLB-Cache)中
    (2)有效位为0,表示虚拟页未被创建且没有占用内存(或者CPU高速缓存TLB-Cache),或者表示已经创建虚拟页但是并没有存储到内存(或者CPU高速缓存TLB-Cache)中
    通过上述的标识位,可以将虚拟页集合分成三个子集,下表所示。
有效位 集合特征
1 虚拟内存已创建和分配页,已缓存在物理内存中。
0 虚拟内存还未分配或创建。
0 虚拟内存已创建和分配页,但未缓存在物理内存中。

4. CPU访问内存过程

当某个进程进行一次内存访问指令请求,将触发上图的内存访问,具体的访问流程如下:

  1. 进程将访问内存相关的指令请求发送给CPU,CPU接受到指令请求
  2. CPU找到数据的虚拟地址(可能存放在寄存器内,所以这一步就已经包括寄存器的全部工作了。)
  3. 将虚拟地址(Virtual Page Numberoffset仅是其中一部分,我们这里只展示这两部分的作用)送往内存管理单元(MMU)
  4. MMU先判断TLB(Translation Look-aside Buffer)中是否缓存了该虚拟地址的物理地址,如果命中,MMU直接获取物理地址
  5. 如果TLB未命中,则将虚拟地址发送给Table Walk Unit
  6. Table Walk Unit根据虚拟地址的VPN获取到一级页表(页目录),再从一级页表中获取到二级页表,从二级页表中获取到对应的物理内存页地址,结合虚拟地址中的物理内存页偏移量offset,拿到物理内存页中其中1项的物理地址
  7. 如果MMU未能查到物理地址,则会触发缺页异常;缺页异常被捕获后,操作系统会根据缺页异常类型,做出不同的处理。
  8. 如果MMU获取到了物理地址,则根据物理地址到Cache中查看是否已缓存了对应的内存数据,如果缓存了则返回内存数据
  9. 如果Cache未命中,则直接拿物理地址到主存中查看是否存在内存数据,如果缓存了则返回内存数据

5. 局部性

一个优秀的程序通常具有良好的局部性,它们通常会重复使用已用过的数据,或者使用已用过数据的邻近数据,也就是说,程序常常会使用集中在一起的局部数据。局部性分为:时间局部性和空间局部性。

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

武昌库里写JAVA

您的鼓励将是我前进的动力!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值