天天看點

0x00000000指令引用的記憶體不能為written_Golang記憶體管理

實體記憶體與虛拟記憶體

0. CPU與記憶體

記憶體的最小存儲機關為一個位元組,我們通過一個十六進制的資料表示每個位元組的編号(比如

4G

記憶體的十六進制表示從

0x0000 0000

0xffff ffff

)。其中記憶體位址的編号上限由位址總線

address bus

的位數相關,

CPU

通過位址總線來表示向記憶體說明想要存儲資料的位址。

32

CPU

為例,

CPU

包含

32

個針腳來傳遞位址資訊,每個針腳能傳遞的資訊為

1bit

(即

0 | 1

),對應的位址空間大小為

2^32 = 4G

0x00000000指令引用的記憶體不能為written_Golang記憶體管理

同其他存儲媒體相比,記憶體的存儲單元采用了随機讀取存儲器

RAM, Random Access Memory

,存儲器讀取資料花費的時間和資料所在的位置無關(而磁盤和錄音帶隻能順序通路資料,有損于速度和效率)。這個特性使得系統可以把控程序的運作時間,這也是記憶體稱為主存儲器的關鍵因素。記憶體的缺點是不能持久化資料,計算機一旦斷電記憶體中的資料就會消失。

1. 早期的記憶體配置設定方式

在一開始是沒有虛拟記憶體的概念的,程式通路的位址就是真實的實體位址,

CPU

可以直接根據寄存器中的值去通路對應的實體記憶體。計算機按照每個程式需要的記憶體大小進行配置設定,這種做法雖然看上去比較直覺,但是存在如下幾個問題:

  1. 程式員需要額外關心記憶體布局:假設記憶體總大小為

    n

    ,第一個程式使用了

    m

    數量的記憶體空間後,第二個程式使用的記憶體範圍即為

    m~n

    ,需要妥善處理記憶體偏移
  2. 記憶體使用效率低下:假如系統沒有足夠的空間運作新的程式時,需要選擇一個已運作的程式拷貝到硬碟上釋放出記憶體空間供新的程式使用,這種多餘的

    IO

    操作會降低計算機的運作效率
  3. 安全風險較高:不同程序的位址空間不隔離,一方面難以阻止惡意程式修改别的程序記憶體資料,另一方面某個程式的

    bug

    也可能會污染其他程序的記憶體資料
0x00000000指令引用的記憶體不能為written_Golang記憶體管理

2. 虛拟記憶體

虛拟記憶體在程式通路真實的實體位址之間封裝了一層,每個程序的記憶體讀寫都是建立在屬于該程序的虛拟記憶體上的,而虛拟記憶體映射到實體記憶體的工作統一交給作業系統處理。程式員既不需要擔心不同程序的記憶體位址沖突的問題,又不需要關心硬體層面上虛拟記憶體是如何映射到實體記憶體的。

0x00000000指令引用的記憶體不能為written_Golang記憶體管理

這種映射帶來的好處包括:

  • 以程式員的視角來看,每個程式申請的虛拟記憶體都是從 開始的連續空間,既不需要再去手動維護記憶體位址的偏移,也不需要擔心不同程式之間的記憶體位址沖突
  • 突破了實體記憶體大小的限制:作業系統配置設定給每個程序的虛拟程序可以比實際的實體記憶體大得多
  • 虛拟記憶體中的連續空間不需要映射到實體記憶體中的連續空間,可以有效利用記憶體碎片

3. 程序與虛拟位址的關系

Linux

程序:程式編譯後的可執行檔案是放在磁盤上的,執行時首先将程序加載到記憶體中然後再放到寄存器中,最後讓

CPU

執行程式,一個靜态的程式就變成了程序。

程序在運作的時候相關資料需要存儲在記憶體中,但是出于資料隔離性和安全性的考慮程序不能通過

0x 0000 0001

的方式直接通路實體記憶體,隻能通過虛拟位址通路。以

Linux

為例,每個程序都有屬于自己的

4GB

虛拟記憶體空間,虛拟位址到實體位址的翻譯和不同程序的實體位址隔離是交給作業系統來實作的。

虛拟位址的實作禁止了程序空間通路實體記憶體位址,統一交給作業系統進行管理,這帶來幾個好處:

  • 對于每個線程獨享的資料,作業系統隻需要将每個程序對應的虛拟空間位址映射到不同的實體空間位址就可以保證兩個程序的執行互不幹擾
  • 對于需要”記憶體共享“的資料實作起來也很簡單:隻需要把多個程序的虛拟記憶體空間對應到同一實體記憶體空間即可(比如記憶體和共享庫的實作)。
0x00000000指令引用的記憶體不能為written_Golang記憶體管理

4. 分頁

分頁的核心思想是可執行檔案的虛拟程序空間分成大小相同的多頁,沒執行到的頁暫時保留在硬碟上,執行到哪一頁就為該頁在實體位址空間中配置設定記憶體,同時建立虛拟位址和實體位址頁的映射關系。

盡管在真實的實體位址上封裝了一層虛拟位址提高了程式設計的效率和安全性,但是作業系統需要額外耗費資源把程序的虛拟位址翻譯成實體位址。在虛拟位址映射到實體位址的過程中,有幾個問題需要考慮到:

  • 為了高效地“翻譯”虛拟位址,這個映射關系必須加載在記憶體中
  • 程式在運作的時候具有局部性特點:在程式運作的某個具體的時間點隻有小部分的資料會被通路到
  • 如果虛拟位址中每個位元組都維護一個對應記錄的話,那麼光是對應關系就已經耗費完記憶體的空間了

基于上述事實,

Linux

引入“分頁”

paging

的方式來記錄對應關系,即使用更大尺寸(一般為

4KB

)的機關

page

來管理記憶體,

Linux

中實體記憶體和程序虛拟記憶體都被分割成頁。

paging

具有如下特點:

  • 一頁之内的位址是連續的:這樣虛拟頁和實體頁一旦聯系到一起,就意味着頁内的順序可以按照順序一一對應起來
  • 可以極大減少虛拟記憶體和實體記憶體的對應關系,如果頁的大小是

    4KB

    ,那麼需要維護的對應關系就減少為​
  • 由于

    4096

    2

    12

    次方,是以位址最後

    12

    位可以同時表示虛拟位址和實體位址的偏移量

    offset

    ,即該位元組在頁内的位置,位址的前一部分表示頁編号,作業系統就隻需要維護虛拟位址和實體位址的頁編号對應關系
圖源: https://www. cnblogs.com/vamei/p/932 9278.html
0x00000000指令引用的記憶體不能為written_Golang記憶體管理

當程序需要讀寫記憶體時,首先喚醒

MMU

根據虛拟空間位址的頁編号映射到真實的實體頁,再根據偏移量找到實際的實體位址進行讀寫。如果程式嘗試去通路一個沒有映射到實體頁的虛拟位址時(這種情況被稱為缺頁中斷),作業系統會建立虛拟位址和實體位址的映射關系到頁表中,如果實體記憶體不夠用時作業系統會覆寫掉某個頁(被覆寫的頁如果曾經被修改過,那麼需要将此頁寫回磁盤)。

Q:從這個角度說,在運作程式的時候,磁盤上可能存儲着除可執行檔案本身外的資料内容,即

4G

虛拟記憶體的一部分?

虛拟記憶體分布

我們編寫的程式都運作在計算機的記憶體(一般指的是計算機的随機存儲器

RAM

),

UNIX

環境記憶體的大緻分布如下:

當應用程式被加載到記憶體空間中執行時,作業系統負責代碼段、資料段和

BSS

段的加載,并在記憶體中為這些段配置設定空間。在程式運作期間,棧段也交給作業系統進行管理,隻有堆段是程式員自行管理(可顯式地申請和釋放空間)的記憶體區域。
0x00000000指令引用的記憶體不能為written_Golang記憶體管理

1. 各個記憶體段的内容

  • 代碼段

    Text Segment

    :存放程式執行代碼的一段記憶體區域,這部分區域的大小在程式運作前就已經确定,并且記憶體區域通常屬于隻讀(不過某些架構也允許修改程式)。代碼段中的指令包含操作碼和被操作對象(或對象位址引用)。隻讀常量将直接包含在代碼段中;局部變量會在棧區配置設定記憶體空間并引用;全局變量會引用

    BSS

    段和資料段中的位址。
  • 資料段

    Data Segment

    :靜态記憶體配置設定,存放靜态變量、初始化的全局變量和常量。
  • BSS

    段:靜态記憶體配置設定,包含了程式中未初始化的全部變量,在記憶體中

    BSS

    段全部置零。
  • heap

    :動态記憶體配置設定,當程序調用

    malloc

    等函數配置設定記憶體時,堆會擴張用于存放新申請的變量,使用

    free

    等函數釋放記憶體時,被釋放的記憶體從堆中剔除。
  • stack

    :存放程式的局部變量,即在函數括弧

    {}

    中定義的變量。除此之外,在函數被調用時參數會被壓入棧中,函數調用結束後函數的傳回值也會被存放在棧中。另外,棧具有先進先出的特點,經常用于儲存/恢複調用現場,這也是我們在程式

    crash

    時列印

    stack

    的原因。

2. 彙編基礎

回顧一下基本的彙編指令:

  • call

    :将目前指令的下一條指令的位址儲存到棧中;跳轉至目标函數的位址
  • ret

    :彈出棧頂位址将資料放入

    eip

  • push

    :從棧頂入棧
  • pop

    :從棧底出棧
  • mov

    :将資料從源位址傳送到目标位址

再回顧一下寄存器:

由于

CPU

的運算速度遠高于記憶體的讀寫速度,為了提高效率

CPU

本身自帶一級和二級緩存。不過由于資料在一二級緩存中的位址是不固定的,

CPU

每次讀寫都要尋址也會拖慢速度,是以

CPU

使用寄存器存儲最頻繁讀寫的資料(比如循環變量)。每一個寄存器都有自己的名稱,程式員直接告訴

CPU

去哪個寄存器拿資料,這樣的速度是最快的。
  • 一般寄存器

    AX

    BX

    CX

    DX

    :分别表示累加寄存器、基址寄存器、計數寄存器和資料寄存器。
  • 索引寄存器

    DI

    SI

    :分别表示目的索引寄存器和來源索引寄存器。
  • SP

    :棧頂指針
  • BP

    :棧底指針

3. 棧調用執行個體

C++

為例,看一下這個小程式:

void foo (int n) {
    return;
}
​
void bar (int n) {
    int a = n * 2;
    foo(a);
    return;
}
​
int main() {
    // ...
    bar(10);
    // ...
    return 0;
}
           

https://gcc.godbolt.org/

檢視彙編代碼:

foo(int):
        push    rbp
        mov     rbp, rsp
        mov     DWORD PTR [rbp-4], edi
        nop
        pop     rbp
        ret
bar(int):
        push    rbp
        mov     rbp, rsp
        sub     rsp, 24
        mov     DWORD PTR [rbp-20], edi
        mov     eax, DWORD PTR [rbp-20]
        add     eax, eax
        mov     DWORD PTR [rbp-4], eax
        mov     eax, DWORD PTR [rbp-4]
        mov     edi, eax
        call    foo(int)
        nop
        leave
        ret
main:
        push    rbp
        mov     rbp, rsp
        mov     edi, 10
        call    bar(int)
        mov     eax, 0
        pop     rbp
        ret
           

按照約定,程式從

main

标簽開始執行,然後開始執行前兩行代碼:

push rbp
mov  rbp, rsp
           

BP

SP

都是堆棧的操作寄存器,其中

SP

一直指向棧頂(由于棧是往低位址方向增長的,執行

push

指令入棧時

SP

自動減少,執行

pop

指令出棧時

SP

自動增加),無法暫借使用,我們一般用

EBP

存取堆棧。

push rbp

将目前的

rbp

入棧,

mov rbp, rsp

rsp

的值賦給

rbp

,這樣就可以通過

rbp + ?

來通路函數的參數,以

rbp - ?

通路函數的變量。

// 将10存放在edi寄存器中
mov edi, 10
// 程式尋找bar(int)的标簽, 并為該函數建立一個新的棧幀
call bar(int)
           

跳轉到

bar(int)

函數後開始執行該函數内的代碼:

// 為函數的局部變量儲存一些記憶體, 棧會增長
sub rsp, 24
// 将之前edi中存儲的10寫到位址[rbp-20]中
mov DWORD PTR [rbp-20], edi
// 實作*2的操作
mov eax, DWORD PTR [rbp-20]
add eax, eax
// 将bar()的傳回值入棧
mov DWORD PTR [rbp-4], eax
// 将傳回值20寫入edi, 并調用foo(int)函數
mov eax, DWORD PTR [rbp-4]
mov edi, eax
call foo(int)
           

同樣為

foo(int)

建立一個棧幀,并跳轉到該函數:

// 将edi中的值入棧, 然而後面并沒有用到
mov DWORD PTR [rbp-4], edi
// 空語句
nop 
// rbp出棧, rbp恢複調用前的值
pop rbp
// 彈出傳回位址, 程式傳回到調用函數處并執行下一行指令
ret
           

這時程式繼續執行

bar(int)

剩下的指令:

nop
// leave相當于mov esp, ebp; pop ebp
leave
ret
           

4. 為什麼需要分區

最主要的還是每個區都有自己的特性,可以實作不同的功能:

  • 全局變量和靜态變量在程式的整個生命周期都可以被通路到,是以需要靜态存儲在一個記憶體區域;臨時變量使用完之外沒有存在的意義,需要及時回收以高效利用記憶體
  • 棧維護一個後進先出的隊列,性能優越并且不會産生記憶體碎片,但是對于沒有價值的變量不能及時釋放,會造成記憶體一定程度的浪費。堆允許程式員自行管理記憶體,極大提高了管理變量的自由度,但是如果忘記釋放記憶體可能造成記憶體洩漏。

記憶體配置設定算法

1. 線性配置設定

線性配置設定算法是最簡單的記憶體配置設定算法,通過一個指針指向空閑記憶體的首位址。當使用者程式申請記憶體時,根據使用者申請記憶體的大小傳回指針後面相應大小的區域,并更新指針。這種方法有兩個卓越的優點:

  • 實作成本低
  • 配置設定速度快
0x00000000指令引用的記憶體不能為written_Golang記憶體管理

但這種簡單粗暴的算法也有顯著的缺點,比如當已配置設定記憶體的對象

2

成為垃圾對象時,我們無法回收對象

2

的記憶體用于下一個對象的配置設定。

0x00000000指令引用的記憶體不能為written_Golang記憶體管理

2. 空閑連結清單配置設定

空閑連結清單

Free-List

會用一個連結清單存儲所有的空閑記憶體,當使用者程式申請記憶體時,掃描空閑記憶體連結清單并取出大小滿足要求的記憶體塊。已配置設定記憶體的對象成為垃圾對象時,對應的記憶體塊會重新插入到空閑連結清單中。

0x00000000指令引用的記憶體不能為written_Golang記憶體管理

一般而言空閑連結清單中有很多的記憶體塊都可以滿足使用者程式申請記憶體塊的大小要求,依據選取政策的不同我們将空閑連結清單配置設定劃分為如下幾種:

  • 首先比對

    First-Fit

    :從空閑連結清單頭部開始掃描,傳回第一個滿足大小的空閑記憶體塊
  • 最佳比對

    Best-Fit

    :周遊空閑連結清單,傳回滿足大小的最小空閑記憶體塊
  • 最差比對

    Worst-Fit

    :周遊空閑連結清單,如果找不到正好符合大小的空閑記憶體塊,從最大的空閑記憶體塊中配置設定
  • 隔離比對

    Segregated-Fit

    :将空閑記憶體分為多個存儲固定大小的連結清單,首先根據使用者程式申請的記憶體大小确定合适的連結清單,再從連結清單中擷取空閑記憶體塊

隔離比對綜合了其他比對方法的優勢,減少了對空閑記憶體塊的周遊,

Golang

的記憶體配置設定政策也是基于該比對方法實作的。隔離比對會将記憶體分割成

4B

8B

16B

32B

等多個連結清單,比如當我們向記憶體配置設定器申請

8B

的記憶體時,記憶體配置設定器會從第二個連結清單中找到空閑的記憶體塊。

0x00000000指令引用的記憶體不能為written_Golang記憶體管理

Golang記憶體管理前身:tcmalloc

1. 簡介

tcmalloc

全稱為

thread-caching malloc

,是

google

推出的一種記憶體管理器。按照對象所占記憶體空間的大小,

tcmalloc

将對象劃分為三類:

  • 小對象:​
    0x00000000指令引用的記憶體不能為written_Golang記憶體管理
  • 中對象:​
    0x00000000指令引用的記憶體不能為written_Golang記憶體管理
  • 大對象:​
    0x00000000指令引用的記憶體不能為written_Golang記憶體管理

相關的重要概念:

  • Page

    tcmalloc

    将虛拟位址空間劃分為多個大小相同的頁

    Page

    (大小為

    8KB

  • Span

    :單個

    Span

    可能包含一個或多個

    Page

    ,是

    tcmalloc

    向作業系統申請記憶體的基本機關
  • Size Class

    :對于

    256KB

    以内的小對象,

    tcmalloc

    按照大小劃分了不同的

    Size Class

    ,比如

    8

    位元組、

    16

    位元組和

    32

    位元組等,以此類推。應用程式申請小對象需要的記憶體時,

    tcmalloc

    會将申請的記憶體向上取整到某個

    Size Class

    的大小
  • ThreadCache

    :每個線程自己維護的緩存,裡面對于每個

    Size Class

    都有一個單獨的

    FreeList

    ,緩存了

    n

    個還未被應用程式使用的空閑對象,由于不同線程的

    ThreadCache

    是互相獨立的,是以小對象從

    ThreadCache

    中的

    FreeList

    中存儲或者回收時是不需要加鎖的,提升了執行效率
  • CentralCache

    :同樣對于每個

    Size Class

    都維護了一個

    Central Free List

    來緩存空閑對象,作為各個

    ThreadCache

    擷取空閑對象,每個線程從

    CentralCache

    中取用或者回收對象是需要加鎖的,為了平攤加鎖解鎖的時間開銷,一般一次會取用或者回收多個空閑對象
  • PageHeap

    :當

    Centralache

    中的空閑對象不夠用時會向

    PageHeap

    申請一塊記憶體然後再拆分成各個

    Size Class

    添加到對應的

    CentralFreeist

    中。

    PageHeap

    對不同記憶體塊

    Span

    的大小采用了不同的緩存政策:

    128 Page

    以内的

    Span

    每個大小都用一個連結清單來緩存,超過

    128 Page

    Span

    存儲在一個有序

    set

0x00000000指令引用的記憶體不能為written_Golang記憶體管理

2. 小對象配置設定

對于小于等于

256KB

的小對象,配置設定流程如下:

  • 将對象大小向上取整到對應的

    Size Class

  • 如果

    FreeList

    非空則直接移除

    FreeList

    第一個空閑對象并傳回,配置設定結束
  • 如果

    FreeList

    為空,從

    CentralCache

    中該

    SizeClass

    對應的

    CentralFreeList

    加鎖一次性擷取一堆空閑對象(如果

    CentralFreeList

    也為空的則向

    PageHeap

    申請一個

    Span

    拆分成

    Size Class

    對應大小的空閑對象,放入

    CentralFreeList

    中),将這堆對象(除第一個對象外)放到

    ThreadCache

    Size Class

    對應的

    FreeList

    ,傳回第一個對象,配置設定結束

回收流程如下:

  • 應用程式調用

    free()

    或者

    delete

    觸發回收
  • 将該對象對應的記憶體插入到

    ThreadCache

    中該

    Size Class

    對應的

    FreeList

  • 僅當滿足一定條件時,

    ThreadCache

    中的空閑對象才會回到

    CentralCache

    中作為線程間的公共緩存

3. 中對象配置設定

對于超過

256KB

但又不超過

1MB

(即

128 Pages

)的中對象,配置設定流程如下:

  • 将該對象所需要的記憶體向上取整到

    k(k <=128)

    Page

    ,是以最多會産生

    8KB

    的記憶體碎片
  • PageHeap

    中的

    k pages list

    的連結清單中開始按順序找到一個非空的連結清單(假如是

    n pages list, n>=k

    ),取出這個非空連結清單中的一個

    span

    并拆分成

    k pages

    n-k pages

    的兩個

    span

    ,前者作為配置設定結果傳回,後者插入到

    n-k pages list

  • 如果一直找到

    128 pages list

    都沒找到非空連結清單,則把這次配置設定當成大對象配置設定

4. 大對象配置設定

對于超過

128 pages

的大對象,配置設定政策如下:

  • 将該對象所需要的記憶體向上取整到

    k

    page

  • 搜尋

    large span set

    ,找到不小于

    k

    page

    的最小

    span

    (假如是

    n pages

    ),将該

    span

    拆成

    k pages

    n-k pages

    的兩個

    span

    ,前者作為結果傳回,後者根據是否大于

    128 pages

    選擇插入到

    n-k pages list

    或者

    large span set

  • 如果找不到合适的

    span

    ,使用

    sbrk

    或者

    mmap

    向系統申請新的記憶體生成新的

    span

    ,再執行一次大對象的配置設定算法

Golang記憶體管理

1.記憶體管理概覽

Golang

的記憶體管理包含記憶體管理單元、線程緩存、中心緩存和頁堆四個重要的元件,分别對應

runtine.mspan

runtime.mcache

runtime.mcentral

runtime.mheap

每一個

Go

程式在啟動時都會向作業系統申請一塊記憶體(僅僅是虛拟的位址空間,并不會真正配置設定記憶體),在

X64

上申請的記憶體會被分成

512M

16G

512G

的三塊空間,分别對應

spans

bitmap

arena

0x00000000指令引用的記憶體不能為written_Golang記憶體管理
  • arena

    :堆區,運作時該區域每

    8KB

    會被劃分成一個頁,存儲了所有在堆上初始化的對象
  • bitmap

    :辨別

    arena

    中哪些位址儲存了對象,

    bitmap

    中一個位元組的記憶體對應

    arena

    區域中

    4

    個指針大小的記憶體,并标記了是否包含指針和是否掃描的資訊(一個指針大小為

    8B

    ,是以

    bitmap

    的大小為

    512GB/(4*8)=16GB

0x00000000指令引用的記憶體不能為written_Golang記憶體管理
0x00000000指令引用的記憶體不能為written_Golang記憶體管理
  • spans

    :存放

    mspan

    的指針,其中每個

    mspan

    會包含多個頁,

    spans

    中一個指針(

    8B

    )表示

    arena

    中某一個

    page

    8KB

    ),是以

    spans

    的大小為

    512GB/(1024)=512MB

0x00000000指令引用的記憶體不能為written_Golang記憶體管理

2. 對象分級與多級緩存

golang

記憶體管理是在

tcmalloc

基礎上實作的,同樣實作了對象分級:

  • 微對象:​
    0x00000000指令引用的記憶體不能為written_Golang記憶體管理
  • 小對象:​
    0x00000000指令引用的記憶體不能為written_Golang記憶體管理
  • 大對象:​
    0x00000000指令引用的記憶體不能為written_Golang記憶體管理
類似于絕大多數對象是“朝生夕死”的,絕大對數對象的大小都小于

32KB

,對不同大小的對象分級管理并針對性實作對象的配置設定和回收算法有助于提升程式執行效率。

同樣

golang

的記憶體管理也實作了

Thread Cache

Central Cache

PageHeap

的三級緩存,這樣做的好處包括:

  • 對于小對象的配置設定可以直接從

    Thread Cache

    擷取對應的記憶體空間,并且每個

    Thread Cache

    是獨立的是以無需加鎖,極大提高了記憶體申請的效率
  • 絕對多數對象都是小對象,是以這種做法可以保證大部分記憶體申請的操作是高效的

3. 記憶體管理元件

3.1 mspan

0x00000000指令引用的記憶體不能為written_Golang記憶體管理

mspan

是由多個連續的

8KB

的頁組成的記憶體空間,實作上是雙端連結清單:

// go1.13.5
type mspan struct {
    next *mspan     // 連結清單下一個span位址
    prev *mspan     // 連結清單前一個span位址
    list *mSpanList // 連結清單位址, 用于DEBUG
​
    startAddr uintptr // 該span在arena區域的起始位址
    npages    uintptr // 該span占用arena區域page的數量
​
    manualFreeList gclinkptr // 空閑對象清單
​
    // freeindex是0~nelems的位置索引, 标記目前span中下一個空對象索引
    freeindex uintptr 
    // 目前span中管理的對象數
    nelems uintptr 
​
    allocCache uint64  // 從freeindex開始的位标記
    allocBits  *gcBits // 該mspan中對象的位圖
    gcmarkBits *gcBits // 該mspan中标記的位圖,用于垃圾回收
    
    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.
           
  • startAddr

    npages

    :确定該

    mspan

    管理的多個頁在

    arena

    堆中的記憶體位置
  • allocCache

    freeindex

    :當使用者程式或者線程向

    mspan

    申請記憶體時,根據這兩個字段在管理的記憶體中快速查找可以配置設定的空間,如果查詢不到可以配置設定的空間,

    mcache

    會調用

    runtine.mcache.refill

    更新

    mspan

    以滿足為更多對象配置設定記憶體的需求
  • spanclass

    :決定

    mspan

    中存儲對象

    object

    的大小和個數,目前

    golang

    8B

    32KB

    共分

    66

    個類,下圖是每一類大小能存儲的

    object

    大小和數量,以及因為記憶體按頁管理造成的

    tail waster

    和最大記憶體浪費率。
假設一個

mspan

spanclass

4

,那麼它能存儲的對象大小為

33~48B

,能存儲的對象個數為

170

個,并且會在一頁

8KB

的尾部浪費

32B

的記憶體:
0x00000000指令引用的記憶體不能為written_Golang記憶體管理

假設

mspan

中存儲的對象大小均為

33B

,那麼最大的記憶體浪費率

max waste

為:
0x00000000指令引用的記憶體不能為written_Golang記憶體管理
0x00000000指令引用的記憶體不能為written_Golang記憶體管理

golang

在配置設定記憶體時會根據對象的大小來選擇不同

spanclass

span

,比如

17~32Byte

的對象都會使用

spanclass

3

span

。超過

32KB

的對象被稱為“大對象”,

golang

在配置設定這類“大對象”時會直接從

heap

中配置設定一個

spanclass

span

,它隻包含一個大對象。

3.2 線程緩存

由于同一時間内隻能有一個線程通路同一個

P

,是以

P

中的資料不需要加鎖。每個

P

都綁定了

span

的緩存(被稱為

mcache

)用于緩存使用者程式申請的微對象,每一個

mcache

都擁有

67 * 2

mspan

0x00000000指令引用的記憶體不能為written_Golang記憶體管理

scan和noscan的差別:

如果對象包含指針,配置設定對象時會使用scan的span;如果對象不包含指針,則配置設定對象時會使用noscan的span。如此在垃圾回收時對于noscan的span可以不用檢視bitmap來标記子對象,提高GC效率。

type mcache struct {
    alloc [numSpanClasses]*mspan
}
​
numSpanClasses = _NumSizeClasses << 1
           

mcache

在初始化時候是沒有任何

mspan

資源的,在使用過程中動态地向

mcentrak

申請,之後會緩存下來。

3.3 中心緩存

mcentral

為所有線程的提供切分好的

mspan

資源,每個

mcentral

會儲存一種特定大小的全局

mspan

清單,包括已配置設定出去的和未配置設定出去的。

type mcentral struct {
    lock mutex         // 互斥鎖
    sizeclass int32    // 規格
    nonempty mSpanList // 尚有空閑object的mspan連結清單
    empty mSpanList    // 沒有空閑object的mspan連結清單,或者是已被mcache取走的msapn連結清單
    nmalloc uint64     // 已累計配置設定的對象個數
}
           

mcache

mcentral

的互動:

  • mcache

    沒有足夠的

    mspan

    時加鎖從

    mcentral

    nonempty

    中擷取

    mspan

    并從

    nonempty

    删除,取出後加入

    empty

    連結清單
  • mcache

    歸還時将

    mspan

    empty

    連結清單删除并重新添加回

    noempty

3.4 mheap

golang

使用一個

mheap

的全局對象

_mheap

管理堆記憶體。主要功能包括:

  • mecntral

    無可用

    mspan

    時可以向

    mheap

    申請,如果

    mheap

    也沒有資源的話就會向作業系統申請新記憶體
  • 用于大對象的配置設定
type mheap struct {
    lock mutex
    // spans: 指向mspans區域,用于映射mspan和page的關系
    spans []*mspan 
    // 指向bitmap首位址,bitmap是從高位址向低位址增長的
    bitmap uintptr 
​
    // 訓示arena區首位址
    arena_start uintptr 
    // 訓示arena區已使用位址位置
    arena_used  uintptr 
    // 訓示arena區末位址
    arena_end   uintptr 
​
    central [67*2]struct {
        mcentral mcentral
        pad [sys.CacheLineSize - unsafe.Sizeof(mcentral{})%sys.CacheLineSize]byte
    }
}
           

4. 配置設定流程

4.1 不同對象的配置設定流程

大對象:從

mheap

上申請記憶體

微對象:調用

mcache

tiny

配置設定器配置設定記憶體

小對象:根據占用空間向上取整由

mcache

中對應規格的

mspan

申請記憶體,如果沒有合适的

mspan

則一級一級依次通過

mcentral

mheap

甚至作業系統申請記憶體空間

4.2 小對象的配置設定流程

  • 首先從P的線程緩存mcache申請記憶體
  • 然後從mcentral申請記憶體
  • 最後從mheap申請記憶體
  • 如果上述都擷取失敗,則從arena區域申請記憶體
0x00000000指令引用的記憶體不能為written_Golang記憶體管理

golang從堆配置設定對象是通過runtime包中的nowobject函數實作的,函數執行流程如下:

0x00000000指令引用的記憶體不能為written_Golang記憶體管理

Reference

[1] https://tonybai.com/2020/02/20/a-visual-guide-to-golang-memory-allocator-from-ground-up/

[2] https://juejin.im/post/5c888a79e51d456ed11955a8

[3] https://yq.aliyun.com/articles/652551

[4] https://www.cnblogs.com/vamei/p/9329278.html

[5] http://legendtkl.com/2015/12/11/go-memory/

[6] http://goog-perftools.sourceforge.net/doc/tcmalloc.html

[7] https://wallenwang.com/2018/11/tcmalloc/#ftoc-heading-17

[8] https://www.cnblogs.com/xumaojun/p/8547439.html

[9] https://zhuanlan.zhihu.com/p/73468738

繼續閱讀