一般程序的内存分配

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

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

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

Go的内存分配核心思想

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

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

  • 每次从操作系统申请一大块儿的内存,由Go来对这块儿内存做分配,减少系统调用
  • 内存分配算法采用Google的 TCMalloc算法。算法比较复杂,究其原理可自行查阅。其核心思想就是把内存切分的非常的细小,分为多级管理,以降低锁的粒度。
  • 回收对象内存时,并没有将其真正释放掉,只是放回预先分配的大块内存中,以便复用。只有内存闲置过多的时候,才会尝试归还部分内存给操作系统,降低整体开销

Go的内存结构

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

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

arena

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

一类是从管理分配角度,由多个连续的页(page)组成的大块内存:

另一类是从使用角度出发,就是平时咱们所了解的:heap中存在很多"对象":

spans

spans区域,可以认为是用于上面所说的管理分配arena(即heap)的区域。此区域存放了 mspan的指针, mspan是啥后面会讲。spans区域用于表示arena区中的某一页(page)属于哪个 mspan。

mspan可以说是go内存管理的最基本单元,但是内存的使用最终还是要落脚到“对象”上。 mspan和对象是什么关系呢?其实“对象”肯定也放到 page中,毕竟 page是内存存储的基本单元。

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

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

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

可以归结为四个字:按需分配。go将内存块分为大小不同的67种,然后再把这67种大内存块,逐个分为小块(可以近似理解为大小不同的相当于 page)称之为 span(连续的 page),在go语言中就是上文提及的 mspan。

对象分配的时候,根据对象的大小选择大小相近的 span,这样,碎片问题就解决了。

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

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

bitmap 有好几种:Stack, data, and bss bitmaps,再就是这次要说的 heap bitmaps。在此bitmap的做作用是标记标记 arena(即heap)中的对象。一是的标记对应地址中是否存在对象,另外是标记此对象是否被gc标记过。一个功能一个bit位,所以, heap bitmaps用两个bit位。bitmap区域中的一个byte对应arena区域的四个指针大小的内存的结构如下:

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

宏观的图为:

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

arena中包含基本的管理单元和程序运行时候生成的对象或实体,这两部分分别被 spans和 bitmap这两块非heap区域的内存所对应着。逻辑图如下:

spans和bitmap都会根据arena的动态变化而动态调整大小。

内存管理组件

go的内存管理组件主要有: mspan、 mcache、 mcentral和 mheap

  • mspan为内存管理的基础单元,直接存储数据的地方。
  • mcache:每个运行期的goroutine都会绑定的一个 mcache(具体来讲是绑定的GMP并发模型中的P,所以可以无锁分配 mspan,后续还会说到), mcache会分配goroutine运行中所需要的内存空间(即 mspan)。
  • mcentral为所有 mcache切分好后备的 mspan
  • mheap代表Go程序持有的所有堆空间。还会管理闲置的span,需要时向操作系统申请新内存。

mspan

有人会问:mspan结构体存放在哪儿?其实,mspan结构本身的内存是从系统分配的,在此不做过多讨论。 mspan在上文讲 spans的时候具体讲过,就是方便根据对象大小来分配使用的内存块,一共有67种类型;最主要解决的是内存碎片问题,减少了内存碎片,提高了内存使用率。 mspan是双向链表,其中主要的属性如下图所示:

mspan是go中内存管理的基本单元,在上文 spans中其实已经做了详细的解说,在此就不在赘述了。

mcache

为了避免多线程申请内存时不断的加锁,goroutine为每个线程分配了 span内存块的缓存,这个缓存即是 mcache,每个goroutine都会绑定的一个 mcache,各个goroutine申请内存时不存在锁竞争的情况。

如何做到的?

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

然后请看下图:

大体上就是上图这个样子了。注意看我们的 mcache在哪儿呢?就在P上!知道为什么没有锁竞争了吧,因为运行期间一个goroutine只能和一个P关联,而 mcache就在P上,所以,不可能有锁的竞争。

我们再来看看 mcache具体的结构:

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

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

对于 <=32k的对象,将直接通过 mcache分配。

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

  • tinny allocations (size < 16 bytes,no pointers)
  • small allocations (16 bytes < size <= 32k)
  • large allocations (size > 32k)

前两类: tiny allocations和 small allocations是直接通过 mcache来分配的。

对于 tiny allocations的分配,有一个微型分配器 tiny allocator来分配,分配的对象都是不包含指针的,例如一些小的字符串和不包含指针的独立的逃逸变量等。

small allocations的分配,就是 mcache根据对象的大小来找自身存在的大小相匹配 mspan来分配。当 mcach没有可用空间时,会从 mcentral的 mspans列表获取一个新的所需大小规格的 mspan。

mcentral

为所有 mcache提供切分好的 mspan。每个 mcentral保存一种特定类型的全局 mspan列表,包括已分配出去的和未分配出去的。

还记得 mspan的67种类型吗?有多少种类型的 mspan就有多少个 mcentral。

每个 mcentral都会包含两个 mspan的列表:

  • 没有空闲对象或 mspan已经被 mcache缓存的 mspan列表(empty mspanList)
  • 有空闲对象的 mspan列表(empty mspanList)

由于 mspan是全局的,会被所有的 mcache访问,所以会出现并发性问题,因而 mcentral会存在一个锁。

单个的 mcentral结构如下:

假如需要分配内存时, mcentral没有空闲的 mspan列表了,此时需要向 mheap去获取。

mheap

mheap可以认为是Go程序持有的整个堆空间, mheap全局唯一,可以认为是个全局变量。其结构如下:

mheap包含了除了上文中讲的 mcache之外的一切, mcache是存在于Go的GMP调度模型的P中的,上文中已经讲过了,关于GMP并发模型,可以参考我的文章 https://mp.weixin.qq.com/s/74hbRTQ2TjdH5G9F2of4_g。仔细观察,可以发现 mheap中也存在一个锁lock。这个lock是作用是什么呢?

我们知道,大于32K的对象被定义为大对象,直接通过 mheap分配。这些大对象的申请是由 mcache发出的,而 mcache在P上,程序运行的时候往往会存在多个P,因此,这个内存申请是并发的;所以为了保证线程安全,必须有一个全局锁。

假如需要分配的内存时, mheap中也没有了,则向操作系统申请一系列新的页(最小 1MB)。

Go内存分配流程总结

对象分三种:

  • 微小对象,size < 16B
  • 一般小对象, 16 bytes < size <= 32k
  • 大对象 size > 32k

分配方式分三种:

  • tinny allocations (size < 16 bytes,no pointers) 微型分配器分配。
  • small allocations ( size <= 32k) 正常分配;首先通过计算使用的大小规格,然后使用 mcache 中对应大小规格的块分配
  • large allocations (size > 32k) 大对象分配;直接通过 mheap分配。这些大对象的申请是以一个全局锁为代价的,因此任何给定的时间点只能同时供一个 P 申请。

对象分配:

  • size范围在在( size < 16B),不包含指针的对象。 mcache上的微型分配器分配
  • size范围在(0 < size < 16B), 包含指针的对象:正常分配
  • size范围在(16B < size <= 32KB), : 正常分配
  • size范围在( size > 32KB) : 大对象分配

分配顺序:

  • 首先通过计算使用的大小规格。
  • 然后使用 mcache中对应大小规格的块分配。
  • 如果 mcentral中没有可用的块,则向 mheap申请,并根据算法找到最合适的 mspan。
  • 如果申请到的 mspan 超出申请大小,将会根据需求进行切分,以返回用户所需的页数。剩余的页构成一个新的 mspan 放回 mheap 的空闲列表。
  • 如果 mheap 中没有可用 span,则向操作系统申请一系列新的页(最小 1MB)。
  • 程序在内存中的分布 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/

转自:http://www.sohu.com/a/320047388_657921

图解golang内存分配机制 (转)的更多相关文章

  1. golang内存分配

    golang内存分配 new一个对象的时候,入口函数是malloc.go中的newobject函数 func newobject(typ *_type) unsafe.Pointer { flags ...

  2. 最简单例子图解JVM内存分配和回收

    一.简介 JVM采用分代垃圾回收.在JVM的内存空间中把堆空间分为年老代和年轻代.将大量(据说是90%以上)创建了没多久就会消亡的对象存储在年轻代,而年老代中存放生命周期长久的实例对象.年轻代中又被分 ...

  3. 最简单例子图解JVM内存分配和回收(转)

    本文转自http://ifeve.com/a-simple-example-demo-jvm-allocation-and-gc/ http://www.idouba.net/a-simple-exa ...

  4. memcached学习——memcached的内存分配机制Slab Allocation、内存使用机制LRU、常用监控记录(四)

    内存分配机制Slab Allocation 本文参考博客:https://my.oschina.net/bieber/blog/505458 Memcached的内存分配是以slabs为单位的,会根据 ...

  5. Go语言内存分配机制

    前言: 本文是学习<<go语言程序设计>> -- 清华大学出版社(王鹏 编著) 的2014年1月第一版 做的一些笔记 , 如有侵权, 请告知笔者, 将在24小时内删除, 转载请 ...

  6. Memcache简介 & 内存分配机制

            关于这个东西里面到底应该存放数据网上一直有很多种说法,有的说sql进行md5之后作为键值,结果作为内容存放,也有人说按照业务逻辑错放,反正是炒的不亦乐乎.        本人经过将近2 ...

  7. map的内存分配机制分析

    该程序演示了map在形成的时候对内存的操作和分配. 因为自己对平衡二叉树的创建细节理解不够,还不太明白程序所显示的日志.等我明白了,再来修改这个文档. /* 功能说明: map的内存分配机制分析. 代 ...

  8. list的内存分配机制分析

    该程序演示了list在内存分配时候的问题.里面的备注信息是我的想法. /* 功能说明: list的内存分配机制分析. 代码说明: list所管理的内存地址可以是不连续的.程序在不断的push_back ...

  9. vector的内存分配机制分析

    该程序初步演示了我对vector在分配内存的时候的理解.可能有误差,随着理解的改变,改代码可以被修改. /* 功能说明: vector的内存分配机制分析. 代码说明: vector所管理的内存地址是连 ...

随机推荐

  1. VS 之github

    VS 代码发布到TFS上 1. 登录  visualstudio.com. 进入 https://qgb.visualstudio.com Create Project 这里是相当于新建了一个文件夹 ...

  2. .NET Core 3.0 发布单文件可执行程序

    Windows dotnet publish -r win10-x64 /p:PublishSingleFile=true maxOS dotnet publish -r osx-x64 /p:Pub ...

  3. EF的导航属性

    在EF中,外键被称为导航属性. 在EF core中,查询的时候默认是只查自身而不会去查询外键表的.如果想要让查询结果包含外键实体,则需要使用include方法来让查询结果包含外键实体.如 db.Stu ...

  4. 归并排序C程序详解

    #include <iostream> #include <cstring> #include <cstdlib> using namespace std; //归 ...

  5. PAT Advanced 1155 Heap Paths (30 分)

    In computer science, a heap is a specialized tree-based data structure that satisfies the heap prope ...

  6. 说一下 HashSet 的实现原理?(未完成)

    说一下 HashSet 的实现原理?(未完成)

  7. github(工蜂)密码过期时sourcetree重新登录

  8. IDEA设置CodeGlance颜色

    CodeGlance是IDEA的mini地图插件, 默认情况下, 其颜色和编辑框的颜色基本一致, 而安装CodeGlance就是为了方便滚动框的上下拖拉, 颜色一致的话会将这种CodeGlance比拖 ...

  9. java面试知识记录

    1.数据库 (1)数据库优化      面试求职:数据库常见面试题(数据库优化思路) 数据库优化方案整理 (2)数据库的事务 MySQL——事务(Transaction)详解 MySQL 事务 2.设 ...

  10. 01—mybatis开山篇

    什么是 MyBatis ?        MyBatis 是支持定制化 SQL.存储过程以及高级映射的优秀的持久层框架.MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集.M ...