我们知道所有程序运行都需要使用内存,而内存的管理和分配又是非常重要的,它决定了你的程序能不能在有限的资源内跑的更快。可以设想一下,如果你自己来设计的一个内存分配的规则,会遇到什么问题呢?如果你有了一大块内存你要怎么去合理的分配和使用呢?今天我们通过几张图来看看golang中的内存分配是怎样的。

前置知识:对golang的GPM模型有所了解,对GC有一定的了解,有助于你理解下面的内容。

想一想

我们首先来想一下,如果我们自己来分配内存的时候可能会遇到什么问题。

我想要512G,你能给吗?

操作系统的内存不是你想要多少就给你多少的。比如我跟操作系统说我要512G内存,你赶紧给我,不给我我就掐死你,如果你是操作系统,是不是立马就想把我给结束了?

能随便分割吗?

如果我拿到一块内存,挺大的,你把它想象成一块地,我今天要用这块地的这个部分,肯定是从中间切一块出来用,然后明天要另一个部分,然后再切出来一部分。如果随便切,今天要一块三角形,明天要一块圆形,那么肯定会留有很多小块的地方,那些地方没有办法被合理的使用,就会浪费。等到想再要一块正方形的地的时候发现没地方可以切了。

不用了我需要放回去吗?

如果我占用了很大一块内存资源,然后用完了,现在不需要了,那自私的人肯定想着,我就偷偷一直占用不行吗?显然是不可以的,不然的话你的应用程序就每天占用着一台机器大量的资源不释放,别的人都没得用了,肯定想把你干掉。所以用完了要放回去。


其实上面的问题就是内存分配常见的一些问题,那为了高效、合理利用内存,势必需要一些人的管理和帮助,下面我们就来看看那些在golang中的管理者,看看他们是如何帮助我们去管理和分配内存的。

内存的管理者

-w505
这张图里面就是golang中内存的管理者们,下面我来依次介绍一下

OS

首先是操作系统,他拥有着全部的机器内存,我们的程序必须向它要。但是他是大领导,很忙的,你不能没事总找他要,很烦,所以每次都会向他一大块内存(1M打底)他会给你一票地址,但是实际其实并不会直接给你分配内存,但是你用到了自然会有。

heap

这个是我们程序中最大的内存持有区域,堆,他管理着那些很大的内存块,同时是他向操作系统去申请内存的,全局只有他一个大人物。他还需要将从操作系统拿过来的内存进行一定的划分,划分成一整块一整块的样子方便管理,同时记录内存的使用情况,方便整理和回收。

central

这个是二把手,有很多,他们会负责将内存划分成足够小的单元,同时需要向下提供内存的分配工作,这个时候就需要一些合理的分配措施了,这个我们后面再说。

cache

这个是最后一个小领导了,管理着最终线程需要使用的内存资源,而且每个线程都会有一个独立的cache,一对一绑定,这样使用的时候就会直接从对应的cache中去取来使用,这样的好处是不用和别人发生争抢。如果所有的线程都从一个地方进行取用,那么势必会造成你也要用,我也要用的情况。

总结

从上面的图我们可以基本明白一个总体的思路是说:需要有人总体去把控所有内存资源的使用,做到统一的调度和管理,这样可以方便后续的回收和利用。同时需要下面有人负责最终使用的分配,从而能达到一个内存的快速分配而不发生争抢。

内存的分配结构

我们知道了内存的管理者是谁,那么现在我们再来看看内存到底是怎么划分的,究竟是切成一个个长方形还是切成一个个圆形了呢?
-w594

这张图就表示了整个golang中内存的分配结构长什么样子。

arena

这块区域最大,明显就是用来存放我们最终的对象,里面分成了一个个8K大小的房间,每个房间我们称为page。(这里虽然写了它是512G,但是你心里要有B数,你电脑根本没这么大的内存,其实操作系统只是给了你地址而已)同时几个page组合在一起的大房间又叫做mspan(这个是golang中内存管理的基本单元)

bitmap

然后我们再来看第二大的bitmap,它是用来表示arena中存放的对象的一些信息,包括这个对象GC标志,还有标识这个对象是否包含指针。你肯定就好奇,干嘛要有这个呢?这其实也很好理解,golang在进行垃圾回收的时候是根据引用的可达性分析来确定一个对象是否可以被回收,同时采用的是三色标记法进行标记对象,所以这里需要有bitmap来保存这些信息。(具体如果不清楚垃圾回收的细节可以去看看我之前写的有关垃圾回收的部分)

spans

最后是spans,这里保存了mspan的指针,这个也好理解,为了方便管理那一个个大房间嘛

内存分配

那么最后我们来看看我们创建的一个对象最后究竟会经历些什么,是怎么样分配的呢?
首先要说明的是,golang很聪明的,如果一个变量可以分配在栈上,那么就不会被分配在堆上,这样可以有效的节约资源(具体我后续还会写别的来说明golang中的变量)。总之我们这里讨论的是分配在堆上的情况。
-w794
整个流程差不多类似就是这样,嗯,你只要把内存想象成房间,现在房价那么贵,你懂的

分配流程

  1. 大对象: >32KB 的对象,直接从heap上分配
  2. 小对象: 16B < obj <= 32KB 计算规格在mcache中找到合适大小的mspan进行分配(你有多大就住多大的房子竟可能的不要浪费房子的空间)
  3. 微小对象: <=16B 的对象使用mcache的tiny分配器分配;(如果将多个微小对象组合起来,用单块内存(object)存储,可有效减少内存浪费。)

秉持原则:给到最合适的大小,然后能凑一起的凑一起挤一挤

扩容

如果不够怎么办呢?不够肯定就要扩容了呗,当不够的时候就会向领导上报,逐层上报,最终想办法拿到内存。
如果cache没有相应规格大小的mspan,则向central申请
如果central没有相应规格大小的mspan,则向heap申请
如果heap中也没有合适大小的mspan,则向操作系统申请

回收

最后还要记得,如果你用完了,不用了,会有后台的清洁工来回收掉,最终还是会还回去的。一方面呢:cache用完了还给central,central就可以给别的cache用;central用完了就会还给heap…最终都不用的还给操作系统

总结

至此golang的内存分配也就说的差不多了,其中一些细节可能没有说到,可能你还需要看看别的文章来补一补。总结一下:

  1. 你多大人住多大的房间,不多给
  2. 划分成合理的大小可以一起给一起回收,大小合适的分割才不会浪费
  3. 用完还回去,需要标记怎么样算用完了
  4. 每个人线程有独立的缓冲区来进行快速分配,不用抢来抢去