golang快速入門[5.3]-go語言是如何運行的-內存分配

前文

前言

  • 在上文中,咱們對於內存、虛擬內存、程序等概念作了簡單介紹windows

  • 在本文中,咱們將介紹內存分配以及go語言實現的內存分配方式數組

內存分配

v2-865768245e48efa289c9e1cd52d0af3f_hd.jpg

  • 在上文中,咱們介紹了,從虛擬內存的角度,程序內存大體能夠分爲5個段textdatabssstackheap緩存

  • 其中text段用於程序指令、文字、靜態常量

  • databss段用於存儲全局變量

  • stack段用於存儲函數調用與函數內的變量,stack段的數據能夠被CPU快速訪問,stack段的大小在運行時是不能增長和減小的,銷燬只是經過棧指針的移動來實現的。同時,這也是爲何程序有時候會報錯stack overflow的緣由。

  • stack段的內存分配是編譯器實現的,咱們無需關心。同時stack一般的大小是有限的。

  • 所以對於大內存的分配,或者想手動建立或釋放內存,就只可以對heap段進行操做,這就是俗稱的動態分配內存。例如c語言中的malloccallocfree以及C++中的newdelete

  • 內存的分配屬於操做系統級別的操做、所以不論是cc++語言的分配,最後都須要調用操做系統的接口。以linux爲例,malloc代碼可能調用了操做系統接口mmap分配內存

  • linux操做系統提供的內存分配接口以下:

    • mmap/munmap 映射/釋放 指定大小的內存.

    • brk/sbrk – 改變`data`段`結束的位置來擴展heap段的內存

    • madvise – 給操做系統建議如何管理內存

    • set_thread_area/get_thread_area – 操做線程本地存儲空間


  • 動態內存分配是操做系統爲咱們作的事情,其效率直接影響到運行在操做系統上的程序。對於通常的程序來講,例如c語言中實現的malloc,最後都是經過調用操做系統的接口來實現的。

  • 動態內存的調度是一個艱難複雜的話題,其要實現的目標包括:

    • 快速分配和釋放

    • 內存開銷小

    • 使用全部內存

    • 避免碎片化


  • 內存分配的算法包括了:

    • K&R malloc

    • Region-based allocator

    • Buddy allocator

    • dlmalloc

    • slab allocator


  • 同時,因爲算法解決的目標等不一樣,還會有不一樣的變種,其餘的目標包括:

    • 內存開銷小(例如buddy的元數據很大)

    • 良好的內存位置

    • cpu核心增長時,擴展性好

    • 併發malloc / free


  • GO語言在進行動態內存分配時,實質調用了上面的操做系統接口。因爲Go語言並無調用c語言的malloc等函數來分配,組織內存,所以,其必須實現本身的內存組織和調度方式。

  • GO語言借鑑了TCMalloc(Thread-Caching Malloc)的內存分配方式

TCMalloc(Thread-Caching Malloc)

  • TCMalloc是一種內存分配算法,比GNU C庫中的malloc要快2倍,正如其名字同樣,其是對於每個線程構建了緩存內存。

  • TCMalloc解決了多線程時內存分配的鎖競爭問題

  • TCMalloc對於小對象的分配很是高效

  • TCMalloc的核心思想是將內存劃分爲多個級別,以減小鎖的粒度。在TCMalloc內部,內存管理分爲兩部分:小對象內存(thread memory)和大對象內存(page heap)。

  • 小對象內存管理將內存頁分紅多個固定大小的可分配的free列表。所以,每一個線程都會有一個無鎖的小對象緩存,這使得在並行程序下分配小對象(<= 32k)很是有效。下圖的對象表明的是字節。

v2-e76ca815be51b4186a9d6e966d00132b_hd.jpg

  • 分配小對象時

    • 咱們將在相同大小的線程本地free list中查找,若是有,則從列表中刪除第一個對象並返回它

    • 若是free list中爲空,咱們從中央free list中獲取對象(中央free list由全部線程共享),將它們放在線程本地free list中,並返回其中一個對象

    • 若是中央free list也爲空,將從中央頁分配器中分配內存頁,並將其分割爲一組相同大小的對象,並將新對象放在中央free list中。和以前同樣,將其中一些對象移動到線程本地空閒列表中


  • 大對象內存管理由集合組成,將其稱爲頁堆(page heap)當分配的對象大於32K時,將使用大對象分配方式。

v2-d655e035755b60c94e5b7c5382c022ca_hd.jpg

  • 第k個free list列表是包含k大小的free list。第256個列表比較特殊,是長度大於等於256頁的free list。

  • 分配大對象時,對於知足k大小頁的分配

    • 咱們在第k個free list中查找

    • 若是該free list爲空,則咱們查找下一個更大的free list,依此類推,最終,若有必要,咱們將查找最後一個空閒列表。若是更大的free list符合條件,則會進行內存分割以符合當前大小。

    • 若是失敗,咱們將從操做系統中獲取內存。


  • 內存是經過連續頁(稱爲Spans)的運行來管理的(Go也根據Spans來管理內存)

  • 在TCMalloc中,span有兩種狀態,已分配或是free狀態。若是爲free,則span是位於頁堆列表中的一個。若是已分配,則它要麼是已移交給應用程序的大對象,要麼是已分紅多個小對象的序列。

v2-abb8092897e4aa5deba3e9b363ce4edf_hd.jpg

  • go內存分配器最初是基於TCMalloc的

go內存分配

  • Go allocator與TCMalloc相似,內存的管理由一系列(spans/mspan對象)組成,使用(線程/協程)本地緩存並根據內存大小進行劃分。

mspan

  • 在go語言中,Spans是8K或更大的連續內存區域。能夠在runtime/mheap.go中對應的mspan結構

type mspan struct {
    next *mspan     // next span in list, or nil if none
    prev *mspan     // previous span in list, or nil if none
    list *mSpanList // For debugging. TODO: Remove.
    startAddr uintptr // address of first byte of span aka s.base()
    npages    uintptr // number of pages in span
    manualFreeList gclinkptr // list of free objects in mSpanManual spans
    freeindex uintptr
    nelems uintptr // number of object in the span.
    allocCache uint64
    allocBits  *gcBits
    gcmarkBits *gcBits
    sweepgen    uint32
    divMul      uint16        // for divide by elemsize - divMagic.mul
    baseMask    uint16        // if non-0, elemsize is a power of 2, & this will get object allocation base
    allocCount  uint16        // number of allocated objects
    spanclass   spanClass     // size class and noscan (uint8)
    state       mSpanStateBox // mSpanInUse etc; accessed atomically (get/set methods)
    needzero    uint8         // needs to be zeroed before allocation
    divShift    uint8         // for divide by elemsize - divMagic.shift
    divShift2   uint8         // for divide by elemsize - divMagic.shift2
    elemsize    uintptr       // computed from sizeclass or from npages
    limit       uintptr       // end of data in span
    speciallock mutex         // guards specials list
    specials    *special      // linked list of special records sorted by offset.
}

v2-0b9c76b0539cd60b705da5b88195e677_hd.jpg

  • 如上圖,mspan是一個雙向連接列表對象,其中包含頁面的起始地址,它具備的頁的數量以及其大小。

  • mspan有三種類型,分別是:

    • idle:沒有對象,能夠釋放回操做系統;或從新用於堆內存;或從新用於棧內存

    • in use:至少具備一個堆對象,而且可能有更多空間

    • stack:用於協程棧。能夠存在於棧中,也能夠存在於堆中,但不能同時存在於二者中。


mcache

  • Go 像 TCMalloc 同樣爲每個 邏輯處理器(P)(Logical Processors) 提供一個本地線程緩存(Local Thread Cache)稱做 mcache,因此若是 Goroutine 須要內存能夠直接從 mcache 中獲取,因爲在同一時間只有一個 Goroutine 運行在 邏輯處理器(P)(Logical Processors) 上,因此中間不須要任何鎖的參與。mcache 包含全部大小規格的 mspan 做爲緩存。

v2-59c9d4bacef7757b8961b53b8aa5962a_hd.jpg

  • 對於每一種大小規格都有兩個類型:

    • scan -- 包含指針的對象。

    • noscan -- 不包含指針的對象。


  • 採用這種方法的好處之一就是進行垃圾回收時 noscan 對象無需進一步掃描是否引用其餘活躍的對象。

mcentral

  • mcentral是被全部邏輯處理器共享的

  • mcentral 對象收集全部給定規格大小的 span。每個 mcentral 都包含兩個 mspan 的列表:

    • empty mspanList -- 沒有空閒對象或 span 已經被 mcache 緩存的 span 列表

    • nonempty mspanList -- 有空閒對象的 span 列表

v2-d80b53e52626b477e2a08fe7b8d59f97_hd.jpg

  • 每個 mcentral 結構體都維護在 mheap 結構體內。

mheap

v2-4691030fac495691744a883e6b6eed5c_hd.jpg

  • Go 使用 mheap 對象管理堆,只有一個全局變量。持有虛擬地址空間。

  • 就上咱們從上圖看到的:mheap 存儲了 mcentral 的數組。這個數組包含了各個的 span 的 mcentral。

central [numSpanClasses]struct {
    mcentral mcentral
    pad      [unsafe.Sizeof(mcentral{})%sys.CacheLineSize]byte
}
  • 因爲咱們有各個規格的 span 的 mcentral,當一個 mcache 從 mcentral 申請 mspan 時,只須要在獨立的 mcentral 級別中使用鎖,因此其它任何 mcache 在同一時間申請不一樣大小規格的 mspan 將互不受影響能夠正常申請。

  • pad爲格外增長的字節。對齊填充(Pad)用於確保 mcentrals 以 CacheLineSize 個字節數分隔,因此每個 MCentral.lock 均可以獲取本身的緩存行(cache line),以免僞共享(false sharing)問題。

  • 圖中對應的free[_MaxMHeapList]mSpanList:一個 spanList 數組。每個 spanList 中的 mspan 包含 1 ~ 127(_MaxMHeapList - 1)個頁。例如,free[3] 是一個包含 3 個頁的 mspan 鏈表。free 表示 free list,表示未分配。對應 busy list。

  • freelarge mSpanList:一個 mspan 的列表,每個元素(mspan)的頁數大於 127,經過 mtreap 結構體管理。busylarge與之相對應。

  • 在進行內存分配時,go按照大小分紅3種對象類

    • 小於16個字節的對象Tiny類

    • 適用於最大32 kB的Small類

    • 適用於大對象的large類


  • Small類會被分爲大約有70個大小,每個大小都擁有一個free list

  • 引入Tiny這一微小對象是爲了適應小字符串和獨立的轉義變量。

  • Tiny微小對象將幾個微小的分配請求組合到一個16字節的內存塊中

  • 當分配Tiny對象時:

    • 查看協程的mcache的相應tiny槽

    • 根據分配對象的大小,將現有子對象(若是存在)的大小四捨五入爲八、4或2個字節

    • 若是當前分配對象與現有tiny子對象適合,請將其放置在此處


  • 若是tiny槽未發現合適的塊:

    • 查看協程的mcache中相應的mspan

    • 掃描mspanbitmap以找到可用插槽

    • 若是有空閒插槽,對其進行分配並將其用做新的小型插槽對象(這一切均可以在不獲取鎖的狀況下完成)


  • 若是mspan沒有可用插槽:

    • mcentral的所需大小類的mspan列表中得到一個新的mspan


  • 若是mspan的列表爲空:

    • mheap獲取內存頁以用於mspan


  • 若是mheap爲空或沒有足夠大的內存頁

    • 從操做系統中分配一組新的頁(至少1MB)

    • Go 會在操做系統分配超大的頁(稱做 arena),分配大量內存頁將分攤與OS溝通的成本


  • small對象分配與Tiny對象相似,

  • 分配和釋放大對象直接使用mheap,就像在TCMalloc中同樣,管理了一組free list

  • 大對象被四捨五入爲頁大小(8K)的倍數,在free list中查找第k個free list,若是其爲空,則繼續查找更大的一個free list,直到第128個free list

  • 若是在第127個free list中找不到,咱們在剩餘的大內存頁(mspan.freelarge字段)中查找跨度,若是失敗,則從操做系統獲取

總結

  • Go 內存管理的通常思想是根據分配對象大小的不一樣,使用不一樣的內存結構構建不一樣的內存緩存級別。

  • 將一個從操做系統接收的連續虛擬內存地址分割爲多級緩存來減小鎖的使用,同時根據指定的大小分配內存減小內存碎片以提升內存分配的效率和在內存釋放以後加快 垃圾回收 的速度

  • 下面是Go內存分配的直觀表達

v2-64892ea57e129f85566959b0a7457aa1_hd.jpg

參考資料

相關文章
相關標籤/搜索