實體記憶體與虛拟記憶體
0. CPU與記憶體記憶體的最小存儲機關為一個位元組,我們通過一個十六進制的資料表示每個位元組的編号(比如
4G
記憶體的十六進制表示從
0x0000 0000
到
0xffff ffff
)。其中記憶體位址的編号上限由位址總線
address bus
的位數相關,
CPU
通過位址總線來表示向記憶體說明想要存儲資料的位址。
以位
32
為例,
CPU
包含
CPU
個針腳來傳遞位址資訊,每個針腳能傳遞的資訊為
32
(即
1bit
),對應的位址空間大小為
0 | 1
。
2^32 = 4G
同其他存儲媒體相比,記憶體的存儲單元采用了随機讀取存儲器
RAM, Random Access Memory
,存儲器讀取資料花費的時間和資料所在的位置無關(而磁盤和錄音帶隻能順序通路資料,有損于速度和效率)。這個特性使得系統可以把控程序的運作時間,這也是記憶體稱為主存儲器的關鍵因素。記憶體的缺點是不能持久化資料,計算機一旦斷電記憶體中的資料就會消失。
1. 早期的記憶體配置設定方式
在一開始是沒有虛拟記憶體的概念的,程式通路的位址就是真實的實體位址,
CPU
可以直接根據寄存器中的值去通路對應的實體記憶體。計算機按照每個程式需要的記憶體大小進行配置設定,這種做法雖然看上去比較直覺,但是存在如下幾個問題:
- 程式員需要額外關心記憶體布局:假設記憶體總大小為
,第一個程式使用了n
數量的記憶體空間後,第二個程式使用的記憶體範圍即為m
,需要妥善處理記憶體偏移m~n
- 記憶體使用效率低下:假如系統沒有足夠的空間運作新的程式時,需要選擇一個已運作的程式拷貝到硬碟上釋放出記憶體空間供新的程式使用,這種多餘的
操作會降低計算機的運作效率IO
- 安全風險較高:不同程序的位址空間不隔離,一方面難以阻止惡意程式修改别的程序記憶體資料,另一方面某個程式的
也可能會污染其他程序的記憶體資料bug
2. 虛拟記憶體
虛拟記憶體在程式通路真實的實體位址之間封裝了一層,每個程序的記憶體讀寫都是建立在屬于該程序的虛拟記憶體上的,而虛拟記憶體映射到實體記憶體的工作統一交給作業系統處理。程式員既不需要擔心不同程序的記憶體位址沖突的問題,又不需要關心硬體層面上虛拟記憶體是如何映射到實體記憶體的。
這種映射帶來的好處包括:
- 以程式員的視角來看,每個程式申請的虛拟記憶體都是從 開始的連續空間,既不需要再去手動維護記憶體位址的偏移,也不需要擔心不同程式之間的記憶體位址沖突
- 突破了實體記憶體大小的限制:作業系統配置設定給每個程序的虛拟程序可以比實際的實體記憶體大得多
- 虛拟記憶體中的連續空間不需要映射到實體記憶體中的連續空間,可以有效利用記憶體碎片
3. 程序與虛拟位址的關系
程序:程式編譯後的可執行檔案是放在磁盤上的,執行時首先将程序加載到記憶體中然後再放到寄存器中,最後讓
Linux
執行程式,一個靜态的程式就變成了程序。
CPU
程序在運作的時候相關資料需要存儲在記憶體中,但是出于資料隔離性和安全性的考慮程序不能通過
0x 0000 0001
的方式直接通路實體記憶體,隻能通過虛拟位址通路。以
Linux
為例,每個程序都有屬于自己的
4GB
虛拟記憶體空間,虛拟位址到實體位址的翻譯和不同程序的實體位址隔離是交給作業系統來實作的。
虛拟位址的實作禁止了程序空間通路實體記憶體位址,統一交給作業系統進行管理,這帶來幾個好處:
- 對于每個線程獨享的資料,作業系統隻需要将每個程序對應的虛拟空間位址映射到不同的實體空間位址就可以保證兩個程序的執行互不幹擾
- 對于需要”記憶體共享“的資料實作起來也很簡單:隻需要把多個程序的虛拟記憶體空間對應到同一實體記憶體空間即可(比如記憶體和共享庫的實作)。
4. 分頁
分頁的核心思想是可執行檔案的虛拟程序空間分成大小相同的多頁,沒執行到的頁暫時保留在硬碟上,執行到哪一頁就為該頁在實體位址空間中配置設定記憶體,同時建立虛拟位址和實體位址頁的映射關系。
盡管在真實的實體位址上封裝了一層虛拟位址提高了程式設計的效率和安全性,但是作業系統需要額外耗費資源把程序的虛拟位址翻譯成實體位址。在虛拟位址映射到實體位址的過程中,有幾個問題需要考慮到:
- 為了高效地“翻譯”虛拟位址,這個映射關系必須加載在記憶體中
- 程式在運作的時候具有局部性特點:在程式運作的某個具體的時間點隻有小部分的資料會被通路到
- 如果虛拟位址中每個位元組都維護一個對應記錄的話,那麼光是對應關系就已經耗費完記憶體的空間了
基于上述事實,
Linux
引入“分頁”
paging
的方式來記錄對應關系,即使用更大尺寸(一般為
4KB
)的機關
page
來管理記憶體,
Linux
中實體記憶體和程序虛拟記憶體都被分割成頁。
paging
具有如下特點:
- 一頁之内的位址是連續的:這樣虛拟頁和實體頁一旦聯系到一起,就意味着頁内的順序可以按照順序一一對應起來
- 可以極大減少虛拟記憶體和實體記憶體的對應關系,如果頁的大小是
,那麼需要維護的對應關系就減少為4KB
- 由于
是4096
的2
次方,是以位址最後12
位可以同時表示虛拟位址和實體位址的偏移量12
,即該位元組在頁内的位置,位址的前一部分表示頁編号,作業系統就隻需要維護虛拟位址和實體位址的頁編号對應關系offset
圖源: https://www. cnblogs.com/vamei/p/932 9278.html
當程序需要讀寫記憶體時,首先喚醒
MMU
根據虛拟空間位址的頁編号映射到真實的實體頁,再根據偏移量找到實際的實體位址進行讀寫。如果程式嘗試去通路一個沒有映射到實體頁的虛拟位址時(這種情況被稱為缺頁中斷),作業系統會建立虛拟位址和實體位址的映射關系到頁表中,如果實體記憶體不夠用時作業系統會覆寫掉某個頁(被覆寫的頁如果曾經被修改過,那麼需要将此頁寫回磁盤)。
Q:從這個角度說,在運作程式的時候,磁盤上可能存儲着除可執行檔案本身外的資料内容,即 4G
虛拟記憶體的一部分?
虛拟記憶體分布
我們編寫的程式都運作在計算機的記憶體(一般指的是計算機的随機存儲器
RAM
),
UNIX
環境記憶體的大緻分布如下:
當應用程式被加載到記憶體空間中執行時,作業系統負責代碼段、資料段和 BSS
段的加載,并在記憶體中為這些段配置設定空間。在程式運作期間,棧段也交給作業系統進行管理,隻有堆段是程式員自行管理(可顯式地申請和釋放空間)的記憶體區域。
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. 線性配置設定
線性配置設定算法是最簡單的記憶體配置設定算法,通過一個指針指向空閑記憶體的首位址。當使用者程式申請記憶體時,根據使用者申請記憶體的大小傳回指針後面相應大小的區域,并更新指針。這種方法有兩個卓越的優點:
- 實作成本低
- 配置設定速度快
但這種簡單粗暴的算法也有顯著的缺點,比如當已配置設定記憶體的對象
2
成為垃圾對象時,我們無法回收對象
2
的記憶體用于下一個對象的配置設定。
2. 空閑連結清單配置設定
空閑連結清單
Free-List
會用一個連結清單存儲所有的空閑記憶體,當使用者程式申請記憶體時,掃描空閑記憶體連結清單并取出大小滿足要求的記憶體塊。已配置設定記憶體的對象成為垃圾對象時,對應的記憶體塊會重新插入到空閑連結清單中。
一般而言空閑連結清單中有很多的記憶體塊都可以滿足使用者程式申請記憶體塊的大小要求,依據選取政策的不同我們将空閑連結清單配置設定劃分為如下幾種:
- 首先比對
:從空閑連結清單頭部開始掃描,傳回第一個滿足大小的空閑記憶體塊First-Fit
- 最佳比對
:周遊空閑連結清單,傳回滿足大小的最小空閑記憶體塊Best-Fit
- 最差比對
:周遊空閑連結清單,如果找不到正好符合大小的空閑記憶體塊,從最大的空閑記憶體塊中配置設定Worst-Fit
- 隔離比對
:将空閑記憶體分為多個存儲固定大小的連結清單,首先根據使用者程式申請的記憶體大小确定合适的連結清單,再從連結清單中擷取空閑記憶體塊Segregated-Fit
隔離比對綜合了其他比對方法的優勢,減少了對空閑記憶體塊的周遊,
Golang
的記憶體配置設定政策也是基于該比對方法實作的。隔離比對會将記憶體分割成
4B
、
8B
、
16B
和
32B
等多個連結清單,比如當我們向記憶體配置設定器申請
8B
的記憶體時,記憶體配置設定器會從第二個連結清單中找到空閑的記憶體塊。
Golang記憶體管理前身:tcmalloc
1. 簡介
tcmalloc
全稱為
thread-caching malloc
,是
google
推出的一種記憶體管理器。按照對象所占記憶體空間的大小,
tcmalloc
将對象劃分為三類:
- 小對象:
- 中對象:
- 大對象:
相關的重要概念:
-
: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
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
。
-
:堆區,運作時該區域每arena
會被劃分成一個頁,存儲了所有在堆上初始化的對象8KB
-
:辨別bitmap
中哪些位址儲存了對象,arena
中一個位元組的記憶體對應bitmap
區域中arena
個指針大小的記憶體,并标記了是否包含指針和是否掃描的資訊(一個指針大小為4
,是以8B
的大小為bitmap
)512GB/(4*8)=16GB
-
:存放spans
的指針,其中每個mspan
會包含多個頁,mspan
中一個指針(spans
)表示8B
中某一個arena
(page
),是以8KB
的大小為spans
512GB/(1024)=512MB
2. 對象分級與多級緩存
golang
記憶體管理是在
tcmalloc
基礎上實作的,同樣實作了對象分級:
- 微對象:
- 小對象:
- 大對象:
類似于絕大多數對象是“朝生夕死”的,絕大對數對象的大小都小于 32KB
,對不同大小的對象分級管理并針對性實作對象的配置設定和回收算法有助于提升程式執行效率。
同樣
golang
的記憶體管理也實作了
Thread Cache
、
Central Cache
和
PageHeap
的三級緩存,這樣做的好處包括:
- 對于小對象的配置設定可以直接從
擷取對應的記憶體空間,并且每個Thread Cache
是獨立的是以無需加鎖,極大提高了記憶體申請的效率Thread Cache
- 絕對多數對象都是小對象,是以這種做法可以保證大部分記憶體申請的操作是高效的
3. 記憶體管理元件
3.1 mspan
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
假設
中存儲的對象大小均為
mspan
,那麼最大的記憶體浪費率
33B
為:
max waste
golang
在配置設定記憶體時會根據對象的大小來選擇不同
spanclass
的
span
,比如
17~32Byte
的對象都會使用
spanclass
為
3
的
span
。超過
32KB
的對象被稱為“大對象”,
golang
在配置設定這類“大對象”時會直接從
heap
中配置設定一個
spanclass
為
的
span
,它隻包含一個大對象。
3.2 線程緩存
由于同一時間内隻能有一個線程通路同一個
P
,是以
P
中的資料不需要加鎖。每個
P
都綁定了
span
的緩存(被稱為
mcache
)用于緩存使用者程式申請的微對象,每一個
mcache
都擁有
67 * 2
個
mspan
。
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區域申請記憶體
golang從堆配置設定對象是通過runtime包中的nowobject函數實作的,函數執行流程如下:
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