溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

golang的內(nèi)存分配

發(fā)布時間:2020-05-28 11:06:22 來源:億速云 閱讀:718 作者:Leah 欄目:編程語言

本篇文章主要介紹golang的內(nèi)存分配,文中關于內(nèi)存分配的算法以及mcache的介紹均以實例展示,有需要的朋友可以參考一下。

golang的內(nèi)存分配

  • 程序內(nèi)存大致可以分為5個段text、data、bssstack、heap

  • 其中text段用于程序指令、文字、靜態(tài)常量

  • databss段用于存儲全局變量

  • stack段用于存儲函數(shù)調(diào)用與函數(shù)內(nèi)的變量,stack段的數(shù)據(jù)可以被CPU快速訪問,stack段的大小在運行時是不能增加和減少的,銷毀只是通過棧指針的移動來實現(xiàn)的。同時,這也是為什么程序有時候會報錯stack overflow的原因。

  • stack段的內(nèi)存分配是編譯器實現(xiàn)的,我們無需關心。同時stack通常的大小是有限的。

  • 因此對于大內(nèi)存的分配,或者想手動創(chuàng)建或釋放內(nèi)存,就只能夠?qū)?code>heap段進行操作,這就是俗稱的動態(tài)分配內(nèi)存。例如c語言中的malloc、calloc、free以及C++中的new、delete

  • 內(nèi)存的分配屬于操作系統(tǒng)級別的操作、因此不管是cc++語言的分配,最后都需要調(diào)用操作系統(tǒng)的接口。以linux為例,malloc代碼可能調(diào)用了操作系統(tǒng)接口mmap分配內(nèi)存

  • linux操作系統(tǒng)提供的內(nèi)存分配接口如下:

    • mmap/munmap 映射/釋放 指定大小的內(nèi)存.

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

    • madvise – 給操作系統(tǒng)建議如何管理內(nèi)存

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


  • 動態(tài)內(nèi)存分配是操作系統(tǒng)為我們做的事情,其效率直接影響到運行在操作系統(tǒng)上的程序。對于一般的程序來說,例如c語言中實現(xiàn)的malloc,最后都是通過調(diào)用操作系統(tǒng)的接口來實現(xiàn)的。

  • 動態(tài)內(nèi)存的調(diào)度是一個艱難復雜的話題,其要實現(xiàn)的目標包括:

    • 快速分配和釋放

    • 內(nèi)存開銷小

    • 使用所有內(nèi)存

    • 避免碎片化


  • 內(nèi)存分配的算法包括了:

    • K&R malloc

    • Region-based allocator

    • Buddy allocator

    • dlmalloc

    • slab allocator


  • 同時,由于算法解決的目標等不同,還會有不同的變種,其他的目標包括:

    • 內(nèi)存開銷?。ɡ鏱uddy的元數(shù)據(jù)很大)

    • 良好的內(nèi)存位置

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

    • 并發(fā)malloc / free


  • GO語言在進行動態(tài)內(nèi)存分配時,實質(zhì)調(diào)用了上面的操作系統(tǒng)接口。由于Go語言并沒有調(diào)用c語言的malloc等函數(shù)來分配,組織內(nèi)存,因此,其必須實現(xiàn)自己的內(nèi)存組織和調(diào)度方式。

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

TCMalloc(Thread-Caching Malloc)

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

  • TCMalloc解決了多線程時內(nèi)存分配的鎖競爭問題

  • TCMalloc對于小對象的分配非常高效

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

  • 小對象內(nèi)存管理將內(nèi)存頁分成多個固定大小的可分配的free列表。因此,每個線程都會有一個無鎖的小對象緩存,這使得在并行程序下分配小對象(<= 32k)非常有效。下圖的對象代表的是字節(jié)。

golang的內(nèi)存分配

  • 分配小對象時

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

    • 如果free list中為空,我們從中央free list中獲取對象(中央free list由所有線程共享),將它們放在線程本地free list中,并返回其中一個對象

    • 如果中央free list也為空,將從中央頁分配器中分配內(nèi)存頁,并將其分割為一組相同大小的對象,并將新對象放在中央free list中。和之前一樣,將其中一些對象移動到線程本地空閑列表中


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

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

  • 分配大對象時,對于滿足k大小頁的分配

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

    • 如果該free list為空,則我們查找下一個更大的free list,依此類推,最終,如有必要,我們將查找最后一個空閑列表。如果更大的free list符合條件,則會進行內(nèi)存分割以符合當前大小。

    • 如果失敗,我們將從操作系統(tǒng)中獲取內(nèi)存。


  • 內(nèi)存是通過連續(xù)頁(稱為Spans)的運行來管理的(Go也根據(jù)Spans來管理內(nèi)存)

  • 在TCMalloc中,span有兩種狀態(tài),已分配或是free狀態(tài)。如果為free,則span是位于頁堆列表中的一個。如果已分配,則它要么是已移交給應用程序的大對象,要么是已分成多個小對象的序列。

golang的內(nèi)存分配

  • go內(nèi)存分配器最初是基于TCMalloc的

  • Go allocator與TCMalloc類似,內(nèi)存的管理由一系列(spans/mspan對象)組成,使用(線程/協(xié)程)本地緩存并根據(jù)內(nèi)存大小進行劃分。

mspan

  • 在go語言中,Spans是8K或更大的連續(xù)內(nèi)存區(qū)域??梢栽?code>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.
}

golang的內(nèi)存分配

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

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

    • idle:沒有對象,可以釋放回操作系統(tǒng);或重新用于堆內(nèi)存;或重新用于棧內(nèi)存

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

    • stack:用于協(xié)程棧??梢源嬖谟跅V校部梢源嬖谟诙阎?,但不能同時存在于兩者中。


mcache

  • Go 像 TCMalloc 一樣為每一個 邏輯處理器(P)(Logical Processors) 提供一個本地線程緩存(Local Thread Cache)稱作 mcache,所以如果 Goroutine 需要內(nèi)存可以直接從 mcache 中獲取,由于在同一時間只有一個 Goroutine 運行在 邏輯處理器(P)(Logical Processors) 上,所以中間不需要任何鎖的參與。mcache 包含所有大小規(guī)格的 mspan 作為緩存。

  • 對于每一種大小規(guī)格都有兩個類型:

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

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


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

mcentral

  • mcentral是被所有邏輯處理器共享的

  • mcentral 對象收集所有給定規(guī)格大小的 span。每一個 mcentral 都包含兩個 mspan 的列表:

    • empty mspanList -- 沒有空閑對象或 span 已經(jīng)被 mcache 緩存的 span 列表

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

golang的內(nèi)存分配

  • 每一個 mcentral 結構體都維護在 mheap 結構體內(nèi)。

mheap

golang的內(nèi)存分配

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

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

central [numSpanClasses]struct {
    mcentral mcentral
    pad      [unsafe.Sizeof(mcentral{})%sys.CacheLineSize]byte
}
  • 由于我們有各個規(guī)格的 span 的 mcentral,當一個 mcache 從 mcentral 申請 mspan 時,只需要在獨立的 mcentral 級別中使用鎖,所以其它任何 mcache 在同一時間申請不同大小規(guī)格的 mspan 將互不受影響可以正常申請。

  • pad為格外增加的字節(jié)。對齊填充(Pad)用于確保 mcentrals 以 CacheLineSize 個字節(jié)數(shù)分隔,所以每一個 MCentral.lock 都可以獲取自己的緩存行(cache line),以避免偽共享(false sharing)問題。

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

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

  • 在進行內(nèi)存分配時,go按照大小分成3種對象類

    • 小于16個字節(jié)的對象Tiny類

    • 適用于最大32 kB的Small類

    • 適用于大對象的large類


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

  • 引入Tiny這一微小對象是為了適應小字符串和獨立的轉(zhuǎn)義變量。

  • Tiny微小對象將幾個微小的分配請求組合到一個16字節(jié)的內(nèi)存塊中

  • 當分配Tiny對象時:

    • 查看協(xié)程的mcache的相應tiny槽

    • 根據(jù)分配對象的大小,將現(xiàn)有子對象(如果存在)的大小四舍五入為8、4或2個字節(jié)

    • 如果當前分配對象與現(xiàn)有tiny子對象適合,請將其放置在此處


  • 如果tiny槽未發(fā)現(xiàn)合適的塊:

    • 查看協(xié)程的mcache中相應的mspan

    • 掃描mspanbitmap以找到可用插槽

    • 如果有空閑插槽,對其進行分配并將其用作新的小型插槽對象(這一切都可以在不獲取鎖的情況下完成)


  • 如果mspan沒有可用插槽:

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


  • 如果mspan的列表為空:

    • mheap獲取內(nèi)存頁以用于mspan


  • 如果mheap為空或沒有足夠大的內(nèi)存頁

    • 從操作系統(tǒng)中分配一組新的頁(至少1MB)

    • Go 會在操作系統(tǒng)分配超大的頁(稱作 arena),分配大量內(nèi)存頁將分攤與OS溝通的成本


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

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

  • 大對象被四舍五入為頁大?。?K)的倍數(shù),在free list中查找第k個free list,如果其為空,則繼續(xù)查找更大的一個free list,直到第128個free list

  • 如果在第127個free list中找不到,我們在剩余的大內(nèi)存頁(mspan.freelarge字段)中查找跨度,如果失敗,則從操作系統(tǒng)獲取。

  • 關于golang的內(nèi)存分配就分享到這里了,希望以上內(nèi)容可以對大家有一定的幫助,可以學到更多知識。如果喜歡這篇文章,不如把它分享出去讓更多的人看到。

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權內(nèi)容。

AI