golang的内存分配(涉及操作系统,MPG调度原理,TCMalloc)

一个golang程序,从编译到运行,发生了什么?

这个问题很有意思,今天我来带大家复习一下基础知识吧。

 

一. 为什么要先编译

  1. 计算机怎么运算的?

    众所周知,计算机只能认出0和1,why??,因为计算机是用电的,电路里只有一个真理,那就是1通电和0不通电,这就可以通过1和0来实现运算器!为了方便操作后面诞生了机器指令

    和汇编,既然这样,那我们的计算机语言是不是都要转为机器指令才能让计算机运行,所以我们需要将golang编译一下生成一个二进制文件,里面包含了机器码

    

 

 

 

  2. 编译期间做了什么?

    go build 一下编译器做了什么,总的来说就是,先从语法词义检查(这就可以bb不同语言不同写法),再到中间码生成,然后优化,最后生成机器码

    ps:java是最终生成字节码byte,然后在jvm上跑,所以哪个快呢?

  

  3. 编译期间分配内存?(go在编译的时候进行逃逸分析,来决定一个对象放栈上还是放堆上)

    1)栈是在编译期间分配内存,堆是在运行期间分配内存,这是网上甚至教科书经常出现的说法。我刚开始很纳闷,为什么你这程序都还没跑就分配内存啦?

    2)其实正确的理解应该是:栈内存不是由编译器分配,而是由编译器确定,并且记录生成到机器码里面,而堆在编译期间是无法确定大小。所以,最终生成二

    进制可执行文件,这东东当然是存在磁盘里面,怎么会占用你的运行内存。

    3)其实当你打开执行了该文件,程序会加载到内存里面,调用函数的时候会拉出一块连续的栈空间,执行完函数后回收,整个过程都是由os来操作,堆就不同了,

    需要写代码时就想好在运行的时候如何动态分配和回收内存。(所以为什么大部分程序崩溃都是运行时出错)

    后面我们讲的golang内存分配主要是发生在堆里!!!

    PS:递归调用的时候要把要把当前变量全都压入栈里,等函数返回后把压入栈的都恢复回来,这个要花一定的时间。递归的时候会发生函数的跳转,这个也费时间。但是这些开销在数据只有几万的时候也不太明显。

  

二. 双击一个golang程序发生了什么

  1. 操作系统做了什么?

    1)首先来科普一下os,os是在cpu之后出现的,那是因为硬件出现了,软件也要跟上时代,要不,谁去管理这些复杂的硬件设施呢。

    2)我们都知道cpu是由运算器,控制器和cache三大部分组成,cache就是寄存器,也可以理解成存储器。

    3)CPU从存储器或高速缓冲存储器中取出指令,放入指令寄存器,并对指令译码。它把指令分解成一系列的微操作,然后发出各种控制命令,执行微操作系列,从而完成一条指令的执行。

    很多人说内存最快,那只是相对于磁盘来说很快,比起寄存器还是差一个级别,寄存器在cpu里(距离最近当然快啦^^)。对不起,跑偏了,下面说说内存吧

  2. 操作系统怎么分配内存?

    1)每一个内存单元是不是都要有个标志,这样才能找到它?是的,这时候地址总线作用就出来了,在存储器里以字节为单位存储信息,为正确地存放或取得信息,每一个字节单元给

    予一个唯一的存储器地址,称为物理地址。32位操作系统最大内存就是2的32次方=4GByte,地址总共4GB个,即可以寻址的空间。

    2)32位操作系统只有4G空间,那么如果我同时启动几个golang程序岂不是乱套了,大家都直接寻址这4G空间,危险而且效率也很低。这时候虚拟内存就出现了,当启动程序的时候分配的

    是虚拟地址,操作系统之上的程序根本就不可能接触物理地址,而且可用的物理地址是相当混乱的,必须由操作系统整理映射。

    3)虚拟地址的0-3G对于一个进程的用户态和内核态来说是可以访问的,而3-4G是只有进程的内核态可以访问的

  3. 操作系统加载内存

    打开程序例如QQ --> CPU将需要的QQ数据从硬盘里拷贝到内存里 --> CPU针对内存里的QQ运算

    有两个重点:

    1)数据从磁盘拷贝到内存:cpu根据虚拟地址映射到物理地址的页表去寻找数据,假如没有,就会引发缺页异常,数据从磁盘拷贝到内存

    2)cpu处理加载后的内存数据:因为如果cpu直接操作磁盘会造成落差很大,cpu速度很快,磁盘很慢,效率非常低,所以为什么会有那么多1级2级3级缓存。

 

   

 

 

 

三. golang运行时内存分配

  前面铺垫得差不多了,下面进入整体,golang的内存分配策略!!!

  1)golang版本:我现在是go version go1.13.5 linux/amd64,为什么要看版本呢

    // This was originally based on tcmalloc, but has diverged quite a bit.(此处diverged翻译为偏差!)
    // http://goog-perftools.sourceforge.net/doc/tcmalloc.html

  2)分配策略TCMalloc:Thread Cache Malloc 线程缓存分配,这为什么和线程挂钩呢,这时就应该引出golang的调度原理了

    golang的代码运行的载体是goroutine,一个main函数就是一个主goroutine,那么goroutine又是靠线程来调度的。

    MPG:M代表线程,P代表处理器,G代表协程

    1. P的数量在初始化由GOMAXPROCS决定,我们要做的就是往p里面添加G;

    2. G的数量超出了M的处理能力,且还有空余P的话,runtime就会自动创建新的M;

    3. M拿到P后才能干活,取G的顺序:本地队列>全局队列>其他P的队列,如果所有队列都没有可用的G,M会归还P并进入休眠;

    假如G发生阻塞会如何:

    

 

 

   如上图,一个G发生阻塞时,M0让出P,由M1接管其任务队列;当M0执行的阻塞调用返回后,再将G0扔到全局队列,自己则进入睡眠(没有P了无法干活);

  

  3)线程是G的载体,内存分配当然是从它说起

      

 

  从thread开始,逐级向上申请内存,那么说刚开始小内存如果能直接在thread获取多好!

  每个线程都会有一个独立的cache,一对一绑定,这样使用的时候就会直接从对应的cache中去取来使用,这样的好处是不用和别人发生争抢(This can all be done without acquiring a lock.)。

  如果所有的线程都从一个地方进行取用,那么势必会造成你也要用,我也要用的情况,说白就是避免了锁的性能消耗

   

  这时查看一下golang1.13.5版本代码,可以发现malloc.go,mheap.go,mcentral.go,mcache.go这4个重要文件,这时可以猜测内存分配的管理者为:

  OS(至少1M) > mheap > mcentral > mcache

  源码注释写得很清楚:

  malloc.go   // Large objects (> 32 kB) are allocated straight from the heap.  

  大对象(> 32 kB)直接从堆mheap中分配。

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

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

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

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

  • large allocations (size > 32k)

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

  4)内存结构(分配好的对象应该放在哪)

  

  所有请求的堆内存都来自于arena。这块区域最大,明显就是用来存放我们最终的对象,里面分成了一个个8K大小的房间,每个房间我们称为page,
  同时几个page组合在一起的大房间又叫做mspanmspan是golang中内存管理的基本单元

  扩容

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

 

四. golang内存回收  

引用计数:对每个对象维护一个引用计数,当引用该对象的对象被销毁时,引用计数减1,当引用计数器为0是回收该对象。
  优点:对象可以很快的被回收,不会出现内存耗尽或达到某个阀值时才回收。
  缺点:不能很好的处理循环引用,而且实时维护引用计数,有也一定的代价。
代表语言:Python、PHP、Swift


标记-清除:从根变量开始遍历所有引用的对象,引用的对象标记为"被引用",没有被标记的进行回收。
  优点:解决了引用计数的缺点。
  缺点:需要STW,即要暂时停掉程序运行。
代表语言:Golang(其采用三色标记法)


分代收集:按照对象生命周期长短划分不同的代空间,生命周期长的放入老年代,而短的放入新生代,不同代有不能的回收算法和回收频率。
  优点:回收性能好
  缺点:算法复杂
代表语言: JAVA

  

三色标记法

2014/6 1.3 引入并发清理(垃圾回收和用户逻辑并发执行?)

2015/8 1.5 引入三色标记法

关于并发清理的引入,参照的是这里在1.3版本中,go runtime分离了mark和sweep的操作,和以前一样,也是先暂停所有任务执行并启动mark(mark这部分还是要把原程序停下来的),

mark完成后就马上就重新启动被暂停的任务了,并且让sweep任务和普通协程任务一样并行,和其他任务一起执行。如果运行在多核处理器上,go会试图将gc任务放到单独的核心上运行

而尽量不影响业务代码的执行,go team自己的说法是减少了50%-70%的暂停时间。

基本算法就是之前提到的清扫+回收,Golang gc优化的核心就是尽量使得STW(Stop The World)的时间越来越短。

 

posted @ 2020-03-24 19:55  天之草  阅读(1871)  评论(0编辑  收藏  举报