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)的時間越來越短。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM