Go内存分配器可视化指南

 go教练   2019-08-09 14:50   97 人阅读  1 条评论

第一次开始尝试理解Go语言的内存分配器时,整个过程让我抓狂。一切看起来都像一个神秘的黑盒子。

因为几乎所有技术魔法(technical wizardry)都隐藏在抽象之下,所以你需要一层一层的剥离才能去理解它。

我们将通过这篇文章来一层层的剥离这些细节。如果你想学习所有关于Go内存分配器可视化指南,那么这篇文章正适合你。

 微信截图_20190809102603.png

物理内存和虚拟内存

每一个内存分配器都需要运行在由底层操作系统管理的虚拟内存空间(Virtual Memory Space)之上。

下图是一个物理内存单元(Physical Memory Cell)的简要说明(非精准)

 微信截图_20190809102629.png

一个内存单元的概述经过大大简化之后描述如下:

地址线(Address line)(晶体管做的开关)用于访问电容器(数据到数据线(Data Lines))。

如果地址线有电流流动(显式为红色),数据线可以写入到电容器,所以电容器带电,逻辑值表示 “1”。

如果地址线没有电流流动(显式为绿色),数据线不可以写入到电容器,所以电容器不带电,逻辑值表示 “0”

CPU需要从RAM中“读取”值,则顺着“地址线(ADDRESS LINE)”(关闭开关)发送一个电流。如果电容器带电,则电流流向“数据线(DATA LINE)”(值为 1);否则没有电流流向数据线,所以电容器保持不带电(值为 0)。

下图简单的描述CPU和物理内存单元如何交互

 微信截图_20190809102647.png

数据总线(Data Bus):用于在CPU和内存中间传输数据。

还有一点关于地址线(Address line)和按字节寻址(Addressable bytes)。

下图是CPU和物理内存之间地址线的说明

 微信截图_20190809102708.png

DRAM中的每一个字节都分配了一个唯一的数字标识符(地址)。“物理字节 != 地址线的数量(Physical bytes present != Number of address line)”(e.g. 16 位 Intel 8088、PAE)

每一个“地址线”可以发送 1-bit 的值,用于表示给定字节地址中的“一个位(SINGLE BIT)”

在我们的上面给出的图中,我们有32个地址线。所以每个字节(BYTE)都有“32 位”作为地址。

[ 00000000000000000000000000000000 ] — 低 内存地址

[ 11111111111111111111111111111111 ] — 高内存地址

4.由于我们每字节都有一个32位的地址,所以我们的地址空间包含2的32次方个可寻址字节(bytes)(4GB)。

综上所述,可寻址的字节数量取决于地址总线的数量,所以对于64个地址线最大可寻址2的64次方个字节数(16 EB),但是由于大部分架构实际上仅使用48-bit地址线(AMD)和 42-bit 地址线(Intel)作为64-bit指针,所以理论上允许 256TB 物理内存(Linux在x86-64下通过with 4 level page tables允许每个处理器128TB地址空间,Windows 192TB)。

由于物理内存的大小是受限制的,所以进程运行在自身的内存沙盒内--“虚拟内存地址(virtual address space)”,称作 虚拟内存(Virtual Memory)。

字节的地址在这个虚拟地址空间内不再和处理器放在地址总线上的地址相同。因此必须建立转换数据结构和系统将虚拟地址空间中的字节映射到物理字节。

虚拟地址表示参见下图(/proc/$PID/maps):

 微信截图_20190809102724.png

综上所述当CPU执行一个指令需要引用内存地址时。首先将在VMA(Virtual Memory Areas)中的逻辑地址转换为线性地址。这个转换通过MMU完成。

 微信截图_20190809102741.png

由于逻辑地址太大几乎很难独立的管理,所以引入术语 页(pages)进行管理。当必要的分页操作被激活后,虚拟地址空间被分成更小的称作页的区域(大部分操作系统下是4KB,可以修改)。页是虚拟内存中数据内存管理的最小单元。虚拟内存不存储任何内容,只是简单的将程序地址空间映射到底层物理内存之上。

独立的进程只能使用VMA作为他们的地址。所以当我们的程序需要更多“堆内存(heap memory)时发生了什么?

下图是简单的汇编代码用于分配更多的堆内存

 微信截图_20190809102759.png

下图描述堆内存的增长

 微信截图_20190809102910.png

应用程序通过系统调用brk(sbrk/mmap等)获得内存。内核仅更新堆VMA并调用它。

当前时间点实际上不分配页帧且新页在物理内存中并不存在。这也是VSZ和RSS大小的不同点。

内存分配器

通过对“虚拟地址空间”基本了解和它对在堆分配的意义,内存分配器现在变得更加容易解释。

如果堆上有足够的空间的满足我们代码的内存申请,内存分配器可以完成内存申请无需内核参与,否则将通过操作系统调用(brk)进行扩展堆,通常是申请一大块内存。(对于malloc大默认指的是大于MMAP_THRESHOLD个字节 - 128KB)。

但是,内存分配器除了更新brk address还有其他职责。其中主要的一项就是如何减少内部(internal)和外部(external)碎片和如何快速分配当前块。考虑我们的程序以串行的方式(p1 到p4)通过 malloc(size) 函数申请一块连续的内存然后通过 free(pointer) 函数进行释放。

 微信截图_20190809102931.png

p4阶段由于内存碎片化即使我们有足够的内存块依然无法满足申请的6个连续的内存块。

所以我们该如何减少内存碎片化呢?答案取决是使用哪种内存分配算法,也就是使用哪个底层库。

我们将简单看一下一个和Go内存分配器建模相近的内存分配器:TCMalloc。

TCMalloc

TCMalloc的核心思想是将内存分为多个级别缩小锁的粒度。在 TCMalloc内存管理内部分为两个部分:线程内存(thread memory)和页堆(page heap)。

线程内存

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

 微信截图_20190809102947.png

页堆

TCMalloc管理的堆由一组页组成,一组连续的页面被表示为 span。当分配的对象大于32KB,将使用页堆(Page Heap)进行内存分配。

 微信截图_20190809102959.png

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

Note: 即使Go的内存分配器最初是基于TCMalloc,但是现在已经有很大的不同。

以上就是今天给大家介绍的Go内存分配器可视化指南如果你还想了解更多关于go语言的知识技巧,可以继续关注我们http://www.fastgolang.com

本文地址:http://www.fastgolang.com/135.html
版权声明:本文为原创文章,版权归 go教练 所有,欢迎分享本文,转载请保留出处!

 发表评论


表情

 评论列表

  1. 动画制作
    动画制作  @回复

    Go语言不错