(十一)golang 內(nèi)存分析

前言

編寫過C語言程序的肯定知道通過malloc()方法動態(tài)申請內(nèi)存用踩,其中內(nèi)存分配器使用的是glibc提供的ptmalloc2孽鸡。 除了glibc,業(yè)界比較出名的內(nèi)存分配器有Google的tcmalloc和Facebook的jemalloc。二者在避免內(nèi)存碎片和性能上均比glic有比較大的優(yōu)勢侧蘸,在多線程環(huán)境中效果更明顯。
Golang中也實現(xiàn)了內(nèi)存分配器鹉梨,原理與tcmalloc類似讳癌,簡單的說就是維護一塊大的全局內(nèi)存,每個線程(Golang中為P)維護一塊小的私有內(nèi)存存皂,私有內(nèi)存不足再從全局申請晌坤。另外,內(nèi)存分配與GC(垃圾回收)關系密切旦袋,所以了解GC前有必要了解內(nèi)存分配的原理骤菠。

基礎概念

為了方便自主管理內(nèi)存,做法便是先向系統(tǒng)申請一塊內(nèi)存疤孕,然后將內(nèi)存切割成小塊商乎,通過一定的內(nèi)存分配算法管理內(nèi)存。 以64位系統(tǒng)為例祭阀,Golang程序啟動時會向系統(tǒng)申請的內(nèi)存如下圖所示:


3DE60CDB-4429-40B7-A149-F9705E914A3A.png

預申請的內(nèi)存劃分為spans鹉戚、bitmap、arena三部分专控。其中arena即為所謂的堆區(qū)抹凳,應用中需要的內(nèi)存從這里分配。其中spans和bitmap是為了管理arena區(qū)而存在的踩官。
arena的大小為512G却桶,為了方便管理把arena區(qū)域劃分成一個個的page,每個page為8KB,一共有512GB/8KB個頁蔗牡;
spans區(qū)域存放span的指針颖系,每個指針對應一個page,所以span區(qū)域的大小為(512GB/8KB)乘以指針大小8byte = 512M
bitmap區(qū)域大小也是通過arena計算出來,不過主要用于GC衬鱼。

span

span是用于管理arena頁的關鍵數(shù)據(jù)結(jié)構糟秘,每個span中包含1個或多個連續(xù)頁,為了滿足小對象分配趁啸,span中的一頁會劃分更小的粒度强缘,而對于大對象比如超過頁大小,則通過多頁實現(xiàn)不傅。

class

根據(jù)對象大小旅掂,劃分了一系列class,每個class都代表一個固定大小的對象访娶,以及每個span的大小商虐。如下表所示:

// class  bytes/obj  bytes/span  objects  waste bytes
//     1          8        8192     1024            0
//     2         16        8192      512            0
//     3         32        8192      256            0
//     4         48        8192      170           32
//     5         64        8192      128            0
//     6         80        8192      102           32
//     7         96        8192       85           32
//     8        112        8192       73           16
//     9        128        8192       64            0
//    10        144        8192       56          128
//    11        160        8192       51           32
//    12        176        8192       46           96
//    13        192        8192       42          128
//    14        208        8192       39           80
//    15        224        8192       36          128
//    16        240        8192       34           32
//    17        256        8192       32            0
//    18        288        8192       28          128
//    19        320        8192       25          192
//    20        352        8192       23           96
//    21        384        8192       21          128
//    22        416        8192       19          288
//    23        448        8192       18          128
//    24        480        8192       17           32
//    25        512        8192       16            0
//    26        576        8192       14          128
//    27        640        8192       12          512
//    28        704        8192       11          448
//    29        768        8192       10          512
//    30        896        8192        9          128
//    31       1024        8192        8            0
//    32       1152        8192        7          128
//    33       1280        8192        6          512
//    34       1408       16384       11          896
//    35       1536        8192        5          512
//    36       1792       16384        9          256
//    37       2048        8192        4            0
//    38       2304       16384        7          256
//    39       2688        8192        3          128
//    40       3072       24576        8            0
//    41       3200       16384        5          384
//    42       3456       24576        7          384
//    43       4096        8192        2            0
//    44       4864       24576        5          256
//    45       5376       16384        3          256
//    46       6144       24576        4            0
//    47       6528       32768        5          128
//    48       6784       40960        6          256
//    49       6912       49152        7          768
//    50       8192        8192        1            0
//    51       9472       57344        6          512
//    52       9728       49152        5          512
//    53      10240       40960        4            0
//    54      10880       32768        3          128
//    55      12288       24576        2            0
//    56      13568       40960        3          256
//    57      14336       57344        4            0
//    58      16384       16384        1            0
//    59      18432       73728        4            0
//    60      19072       57344        3          128
//    61      20480       40960        2            0
//    62      21760       65536        3          256
//    63      24576       24576        1            0
//    64      27264       81920        3          128
//    65      28672       57344        2            0
//    66      32768       32768        1            0

上表中每列含義如下:
class: class ID,每個span結(jié)構中都有一個class ID, 表示該span可處理的對象類型
bytes/obj:該class代表對象的字節(jié)數(shù)
bytes/span:每個span占用堆的字節(jié)數(shù)崖疤,也即頁數(shù)乘以頁大小
objects: 每個span可分配的對象個數(shù)秘车,也即(bytes/spans)/(bytes/obj)waste
bytes: 每個span產(chǎn)生的內(nèi)存碎片,也即(bytes/spans)%(bytes/obj)上表可見最大的對象是32K大小劫哼,超過32K大小的由特殊的class表示叮趴,該class ID為0,每個class只包含一個對象权烧。

span數(shù)據(jù)結(jié)構

span是內(nèi)存管理的基本單位,每個span用于管理特定的class對象, 跟據(jù)對象大小眯亦,span將一個或多個頁拆分成多個塊進行管理。src/runtime/mheap.go:mspan定義了其數(shù)據(jù)結(jié)構:

type mspan struct {
    next *mspan            //鏈表前向指針豪嚎,用于將span鏈接起來
    prev *mspan            //鏈表前向指針搔驼,用于將span鏈接起來
    startAddr uintptr // 起始地址,也即所管理頁的地址
    npages    uintptr // 管理的頁數(shù)

    nelems uintptr // 塊個數(shù)侈询,也即有多少個塊可供分配

    allocBits  *gcBits //分配位圖舌涨,每一位代表一個塊是否已分配

    allocCount  uint16     // 已分配塊的個數(shù)
    spanclass   spanClass  // class表中的class ID

    elemsize    uintptr    // class表中的對象大小,也即塊大小
}

以class 10為例扔字,span和管理的內(nèi)存如下圖所示:


51526E1D-4C0B-4107-9819-3CAC0604FD83.png

spanclass為10囊嘉,參照class表可得出npages=1,nelems=56,elemsize為144。其中startAddr是在span初始化時就指定了某個頁的地址革为。allocBits指向一個位圖扭粱,每位代表一個塊是否被分配,本例中有兩個塊已經(jīng)被分配震檩,其allocCount也為2琢蛤。next和prev用于將多個span鏈接起來,這有利于管理多個span抛虏,接下來會進行說明博其。

cache

有了管理內(nèi)存的基本單位span,還要有個數(shù)據(jù)結(jié)構來管理span迂猴,這個數(shù)據(jù)結(jié)構叫mcentral慕淡,各線程需要內(nèi)存時從mcentral管理的span中申請內(nèi)存,為了避免多線程申請內(nèi)存時不斷的加鎖沸毁,Golang為每個線程分配了span的緩存峰髓,這個緩存即是cache傻寂。src/runtime/mcache.go:mcache定義了cache的數(shù)據(jù)結(jié)構

type mcache struct {
    alloc [67*2]*mspan // 按class分組的mspan列表
}

alloc為mspan的指針數(shù)組,數(shù)組大小為class總數(shù)的2倍携兵。數(shù)組中每個元素代表了一種class類型的span列表疾掰,每種class類型都有兩組span列表,第一組列表中所表示的對象中包含了指針徐紧,第二組列表中所表示的對象不含有指針个绍,這么做是為了提高GC掃描性能,對于不包含指針的span列表浪汪,沒必要去掃描。根據(jù)對象是否包含指針凛虽,將對象分為noscan和scan兩類死遭,其中noscan代表沒有指針,而scan則代表有指針凯旋,需要GC進行掃描呀潭。mcache和span的對應關系如下圖所示:


6370E9B3-E3B7-4319-B733-01D46706380E.png

mchache在初始化時是沒有任何span的,在使用過程中會動態(tài)的從central中獲取并緩存下來至非,跟據(jù)使用情況钠署,每種class的span個數(shù)也不相同。上圖所示荒椭,class 0的span數(shù)比class1的要多谐鼎,說明本線程中分配的小對象要多一些。

central

cache作為線程的私有資源為單個線程服務趣惠,而central則是全局資源狸棍,為多個線程服務,當某個線程內(nèi)存不足時會向central申請味悄,當某個線程釋放內(nèi)存時又會回收進central草戈。src/runtime/mcentral.go:mcentral定義了central數(shù)據(jù)結(jié)構:

type mcentral struct {
    lock      mutex     //互斥鎖
    spanclass spanClass // span class ID
    nonempty  mSpanList // non-empty 指還有空閑塊的span列表
    empty     mSpanList // 指沒有空閑塊的span列表

    nmalloc uint64      // 已累計分配的對象個數(shù)
}

lock: 線程間互斥鎖,防止多線程讀寫沖突
spanclass : 每個mcentral管理著一組有相同class的span列表
nonempty: 指還有內(nèi)存可用的span列表
empty: 指沒有內(nèi)存可用的span列表
nmalloc: 指累計分配的對象個數(shù)線程從central獲取span步驟如下:

  1. 加鎖
  2. 從nonempty列表獲取一個可用span侍瑟,并將其從鏈表中刪除
  3. 將取出的span放入empty鏈表
  4. 將span返回給線程
  5. 解鎖
  6. 線程將該span緩存進cache線程

將span歸還步驟如下:

  1. 加鎖
  2. 將span從empty列表刪除
  3. 將span加入noneempty列表
  4. 解鎖上述線程從central中獲取span和歸還span只是簡單流程唐片,為簡單起見,并未對具體細節(jié)展開涨颜。

heap

從mcentral數(shù)據(jù)結(jié)構可見费韭,每個mcentral對象只管理特定的class規(guī)格的span。事實上每種class都會對應一個mcentral,這個mcentral的集合存放于mheap數(shù)據(jù)結(jié)構中咐低。src/runtime/mheap.go:mheap定義了heap的數(shù)據(jù)結(jié)構:

type mheap struct {
    lock      mutex

    spans []*mspan

    bitmap        uintptr     //指向bitmap首地址揽思,bitmap是從高地址向低地址增長的

    arena_start uintptr        //指示arena區(qū)首地址
    arena_used  uintptr        //指示arena區(qū)已使用地址位置

    central [67*2]struct {
        mcentral mcentral
        pad      [sys.CacheLineSize - unsafe.Sizeof(mcentral{})%sys.CacheLineSize]byte
    }
}

lock: 互斥鎖
spans: 指向spans區(qū)域,用于映射span和page的關系
bitmap:bitmap的起始地址
arena_start: arena區(qū)域首地址
arena_used: 當前arena已使用區(qū)域的最大地址
central: 每種class對應的兩個mcentral
從數(shù)據(jù)結(jié)構可見见擦,mheap管理著全部的內(nèi)存钉汗,事實上Golang就是通過一個mheap類型的全局變量進行內(nèi)存管理的羹令。mheap內(nèi)存管理示意圖如下:


0C8AC0A6-A2CF-424A-96F2-12C0FE7F97C6.png

系統(tǒng)預分配的內(nèi)存分為spans、bitmap损痰、arean三個區(qū)域福侈,通過mheap管理起來。接下來看內(nèi)存分配過程卢未。

內(nèi)存分配過程

針對待分配對象的大小不同有不同的分配邏輯:
(0, 16B) 且不包含指針的對象: Tiny分配
(0, 16B) 包含指針的對象:正常分配
[16B, 32KB] : 正常分配
(32KB, -) : 大對象分配其中Tiny分配和大對象分配都屬于內(nèi)存管理的優(yōu)化范疇肪凛,這里暫時僅關注一般的分配方法。
以申請size為n的內(nèi)存為例辽社,分配步驟如下:

  1. 獲取當前線程的私有緩存mcache
  2. 跟據(jù)size計算出適合的class的ID
  3. 從mcache的alloc[class]鏈表中查詢可用的span
  4. 如果mcache沒有可用的span則從mcentral申請一個新的span加入mcache中
  5. 如果mcentral中也沒有可用的span則從mheap中申請一個新的span加入mcentral
  6. 從該span中獲取到空閑對象地址并返回

總結(jié)

Golang內(nèi)存分配是個相當復雜的過程伟墙,其中還摻雜了GC的處理,這里僅僅對其關鍵數(shù)據(jù)結(jié)構進行了說明滴铅,了解其原理而又不至于深陷實現(xiàn)細節(jié)戳葵。1、Golang程序啟動時申請一大塊內(nèi)存并劃分成spans汉匙、bitmap拱烁、arena區(qū)域
2、arena區(qū)域按頁劃分成一個個小塊噩翠。
3戏自、span管理一個或多個頁。
4伤锚、mcentral管理多個span供線程申請使用
5擅笔、mcache作為線程私有資源,資源來源于mcentral见芹。

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末剂娄,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子玄呛,更是在濱河造成了極大的恐慌阅懦,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,185評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件徘铝,死亡現(xiàn)場離奇詭異耳胎,居然都是意外死亡,警方通過查閱死者的電腦和手機惕它,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評論 3 393
  • 文/潘曉璐 我一進店門怕午,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人淹魄,你說我怎么就攤上這事郁惜。” “怎么了甲锡?”我有些...
    開封第一講書人閱讀 163,524評論 0 353
  • 文/不壞的土叔 我叫張陵兆蕉,是天一觀的道長羽戒。 經(jīng)常有香客問我,道長虎韵,這世上最難降的妖魔是什么易稠? 我笑而不...
    開封第一講書人閱讀 58,339評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮包蓝,結(jié)果婚禮上驶社,老公的妹妹穿的比我還像新娘。我一直安慰自己测萎,他們只是感情好亡电,可當我...
    茶點故事閱讀 67,387評論 6 391
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著硅瞧,像睡著了一般逊抡。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上零酪,一...
    開封第一講書人閱讀 51,287評論 1 301
  • 那天,我揣著相機與錄音拇勃,去河邊找鬼四苇。 笑死,一個胖子當著我的面吹牛方咆,可吹牛的內(nèi)容都是我干的月腋。 我是一名探鬼主播,決...
    沈念sama閱讀 40,130評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼瓣赂,長吁一口氣:“原來是場噩夢啊……” “哼榆骚!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起煌集,我...
    開封第一講書人閱讀 38,985評論 0 275
  • 序言:老撾萬榮一對情侶失蹤妓肢,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后苫纤,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體碉钠,經(jīng)...
    沈念sama閱讀 45,420評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,617評論 3 334
  • 正文 我和宋清朗相戀三年卷拘,在試婚紗的時候發(fā)現(xiàn)自己被綠了喊废。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,779評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡栗弟,死狀恐怖污筷,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情乍赫,我是刑警寧澤瓣蛀,帶...
    沈念sama閱讀 35,477評論 5 345
  • 正文 年R本政府宣布陆蟆,位于F島的核電站,受9級特大地震影響揪惦,放射性物質(zhì)發(fā)生泄漏遍搞。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,088評論 3 328
  • 文/蒙蒙 一器腋、第九天 我趴在偏房一處隱蔽的房頂上張望溪猿。 院中可真熱鬧,春花似錦纫塌、人聲如沸诊县。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,716評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽依痊。三九已至,卻和暖如春怎披,著一層夾襖步出監(jiān)牢的瞬間胸嘁,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,857評論 1 269
  • 我被黑心中介騙來泰國打工凉逛, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留性宏,地道東北人。 一個月前我還...
    沈念sama閱讀 47,876評論 2 370
  • 正文 我出身青樓状飞,卻偏偏與公主長得像毫胜,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子诬辈,可洞房花燭夜當晚...
    茶點故事閱讀 44,700評論 2 354

推薦閱讀更多精彩內(nèi)容

  • Go語言——內(nèi)存管理 參考: 圖解 TCMalloc Golang 內(nèi)存管理 Go 內(nèi)存管理 問題 內(nèi)存碎片:避免...
    陳先生_9e91閱讀 4,807評論 1 10
  • Go語言內(nèi)置運行時(就是runtime)酵使,拋棄了傳統(tǒng)的內(nèi)存分配方式,改為自主管理焙糟。這樣可以自主地實現(xiàn)更好的內(nèi)存使用...
    ddu_sw閱讀 1,374評論 0 5
  • 本文是《循序漸進go語言》第四篇-Go內(nèi)存分配機制口渔。golang的內(nèi)存采用了TCMalloc 這種分配機制。go是...
    鏈人成長chainerup閱讀 3,848評論 1 12
  • GO語言內(nèi)存管理子系統(tǒng)主要由兩部分組成:內(nèi)存分配器和垃圾回收器(gc)穿撮。內(nèi)存分配器主要解決小對象的分配管理和多線程...
    adrian920閱讀 11,196評論 4 6
  • 離溫江10公里左右搓劫,嘉裕?第六洲在難得的冬日陽光下秀著當?shù)厝诵腋I睢?/div>
    guiliqiang閱讀 957評論 0 2