一般程序的内存分配

在讲Golang的内存分配之前,让我们先来看看一般程序的内存分布情况:

以上是程序内存的逻辑分类情况。

我们再来看看一般程序的内存的真实(真实逻辑)图:

Go的内存分配核心思想

Go是内置运行时的编程语言(runtime),像这种内置运行时的编程语言通常会抛弃传统的内存分配方式,改为自己管理。这样可以完成类似预分配、内存池等操作,以避开系统调用带来的性能问题,防止每次分配内存都需要系统调用。

Go的内存分配的核心思想可以分为以下几点:

TCMalloc算法

Go的内存结构

Go在程序启动的时候,会分配一块连续的内存(虚拟内存)。整体如下:

图中span和bitmap的大小会随着heap的改变而改变

arena

arena区域就是我们通常所说的heap。 heap中按照管理和使用两个维度可认为存在两类“东西”:

一类是从管理分配角度,由多个连续的页(page)组成的大块内存:另一类是从使用角度出发,就是平时咱们所了解的:heap中存在很多"对象":

spans

mspanmspanmspan
mspanmspanpagepage

我们抛开问题不看,先看看一般情况下的对象和内存的分配是如何的:如下图

假如再分配“p4”的时候,是不是内存不足没法分配了?是不是有很多碎片?

这种一般的分配情况会出现内存碎片的情况,go是如何解决的呢?

pagespanpagemspanspan

67中不同大小的span代码注释如下(目前版本1.11):

 
// class bytes/obj bytes/span objects tail waste max waste// 1 8 8192 1024 0 87.50%// 2 16 8192 512 0 43.75%// 3 32 8192 256 0 46.88%// 4 48 8192 170 32 31.52%// 5 64 8192 128 0 23.44%// 6 80 8192 102 32 19.07%// 7 96 8192 85 32 15.95%// 8 112 8192 73 16 13.56%// 9 128 8192 64 0 11.72%// 10 144 8192 56 128 11.82%// 11 160 8192 51 32 9.73%// 12 176 8192 46 96 9.59%// 13 192 8192 42 128 9.25%// 14 208 8192 39 80 8.12%// 15 224 8192 36 128 8.15%// 16 240 8192 34 32 6.62%// 17 256 8192 32 0 5.86%// 18 288 8192 28 128 12.16%// 19 320 8192 25 192 11.80%// 20 352 8192 23 96 9.88%// 21 384 8192 21 128 9.51%// 22 416 8192 19 288 10.71%// 23 448 8192 18 128 8.37%// 24 480 8192 17 32 6.82%// 25 512 8192 16 0 6.05%// 26 576 8192 14 128 12.33%// 27 640 8192 12 512 15.48%// 28 704 8192 11 448 13.93%// 29 768 8192 10 512 13.94%// 30 896 8192 9 128 15.52%// 31 1024 8192 8 0 12.40%// 32 1152 8192 7 128 12.41%// 33 1280 8192 6 512 15.55%// 34 1408 16384 11 896 14.00%// 35 1536 8192 5 512 14.00%// 36 1792 16384 9 256 15.57%// 37 2048 8192 4 0 12.45%// 38 2304 16384 7 256 12.46%// 39 2688 8192 3 128 15.59%// 40 3072 24576 8 0 12.47%// 41 3200 16384 5 384 6.22%// 42 3456 24576 7 384 8.83%// 43 4096 8192 2 0 15.60%// 44 4864 24576 5 256 16.65%// 45 5376 16384 3 256 10.92%// 46 6144 24576 4 0 12.48%// 47 6528 32768 5 128 6.23%// 48 6784 40960 6 256 4.36%// 49 6912 49152 7 768 3.37%// 50 8192 8192 1 0 15.61%// 51 9472 57344 6 512 14.28%// 52 9728 49152 5 512 3.64%// 53 10240 40960 4 0 4.99%// 54 10880 32768 3 128 6.24%// 55 12288 24576 2 0 11.45%// 56 13568 40960 3 256 9.99%// 57 14336 57344 4 0 5.35%// 58 16384 16384 1 0 12.49%// 59 18432 73728 4 0 11.11%// 60 19072 57344 3 128 3.57%// 61 20480 40960 2 0 6.87%// 62 21760 65536 3 256 6.25%// 63 24576 24576 1 0 11.45%// 64 27264 81920 3 128 10.00%// 65 28672 57344 2 0 4.91%// 66 32768 32768 1 0 12.50%

说说每列代表的含义:

  • class: class ID,每个span结构中都有一个class ID, 表示该span可处理的对象类型

  • bytes/obj:该class代表对象的字节数

  • bytes/span:每个span占用堆的字节数,也即页数*页大小

  • objects: 每个span可分配的对象个数,也即(bytes/spans)/(bytes/obj)

  • waste bytes: 每个span产生的内存碎片,也即(bytes/spans)%(bytes/obj)

阅读方式如下: 以类型(class)为1的span为例,span中的元素大小是8 byte, span本身占1页也就是8K, 一共可以保存1024个对象。

细心的同学可能会发现代码中一共有66种,还有一种特殊的span: 即对于大于32k的对象出现时,会直接从heap分配一个特殊的span,这个特殊的span的类型(class)是0, 只包含了一个大对象, span的大小由对象的大小决定。

bitmap

heap bitmapsarenaheap bitmaps

bitmap的地址是由高地址向低地址增长的。

宏观的图为:

bitmap 主要的作用还是服务于GC。

arenaspansbitmap

内存管理组件

mspanmcachemcentralmheap
mspanmcachemcachemspanmcachemspanmcentralmcachemspanmheap

mspan

mspanspansmspan
mspanspans

mcache

spanmcachemcache

如何做到的?

在讲之前,请先回顾一下Go的并发调度模型,如果你还不了解,请看我这篇文章 Go并发调度原理

然后请看下图:

mcachemcache
mcache

mcache中的span链表分为两组,一组是包含指针类型的对象,另一组是不包含指针类型的对象。为什么分开呢?

主要是方便GC,在进行垃圾回收的时候,对于不包含指针的对象列表无需进一步扫描是否引用其他活跃的对象(如果对go的gc不是很了解,请看我这篇文章 图解Golang的GC算法)。

<=32kmcache

在此,我觉的有必要说一下go中对象按照的大小维度的分类。 分为三类:

  • tinny allocations (size < 16 bytes,no pointers)

  • small allocations (16 bytes < size <= 32k)

  • large allocations (size > 32k)

tiny allocationssmall allocationsmcache
tiny allocationstiny allocator
small allocationsmcachemspanmcachmcentralmspansmspan

mcentral

mcachemspanmcentralmspan

mspanmspanmcentral
mcentralmspan
mspanmcachemspanmspan
mspanmcachemcentral
mcentral
mcentralmspanmheap

mheap

mheapmheap

mheapmcachemcachemheap
mheapmcachemcache
mheap

Go内存分配流程总结

对象分三种:

  • 微小对象,size < 16B

  • 一般小对象, 16 bytes < size <= 32k

  • 大对象 size > 32k

分配方式分三种:

mheap

对象分配:

mcache

分配顺序:

mcachemcentralmheapmspanmspan

Go的内存管理是非常复杂的,且每个版本都有细微的变化,在此,只讲了些最容易宏观掌握的东西,希望大家多多提意见,如有什么问题,请留言沟通。

问题直接在公众号内留言即可

参考文献:

  • 程序在内存中的分布 https://www.cnblogs.com/Lynn-Zhang/p/5449199.html

  • 从内存分配开始 https://mp.weixin.qq.com/s/EyWKFRu1xryoHY386QUcuA

  • 译文:Go 内存分配器可视化指南 https://www.linuxzen.com/go-memory-allocator-visual-guide.html

  • 图解Go语言内存分配 https://juejin.im/post/5c888a79e51d456ed11955a8

  • Golang源码探索(三) GC的实现原理 https://www.cnblogs.com/zkweb/p/7880099.html

  • 简单易懂的 Go 内存分配原理解读 https://yq.aliyun.com/articles/652551

  • 雨痕<>

  • go内存分配(英文) https://andrestc.com/post/go-memory-allocation-pt1/