前言
編寫過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)存如下圖所示:
預申請的內(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)存如下圖所示:
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的對應關系如下圖所示:
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步驟如下:
- 加鎖
- 從nonempty列表獲取一個可用span侍瑟,并將其從鏈表中刪除
- 將取出的span放入empty鏈表
- 將span返回給線程
- 解鎖
- 線程將該span緩存進cache線程
將span歸還步驟如下:
- 加鎖
- 將span從empty列表刪除
- 將span加入noneempty列表
- 解鎖上述線程從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)存管理示意圖如下:
系統(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)存為例辽社,分配步驟如下:
- 獲取當前線程的私有緩存mcache
- 跟據(jù)size計算出適合的class的ID
- 從mcache的alloc[class]鏈表中查詢可用的span
- 如果mcache沒有可用的span則從mcentral申請一個新的span加入mcache中
- 如果mcentral中也沒有可用的span則從mheap中申請一個新的span加入mcentral
- 從該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见芹。