深入理解golang:記憶體分配原理

只愛宅zmy發表於2020-11-04

一、Linux系統記憶體

在說明golang記憶體分配之前,先了解下Linux系統記憶體相關的基礎知識,有助於理解golang記憶體分配原理。

1.1 虛擬記憶體技術

在早期記憶體管理中,如果程式太大,超過了空閒記憶體容量,就沒有辦法把全部程式裝入到記憶體,這時怎麼辦? 在許多年前,人們採用了一種叫做覆蓋技術,這樣一種解決方案。

這是一種什麼樣的解決方案?
就是把程式分為若干個部分,稱為覆蓋塊(overlay),核心思想就是分解(跟現代架構技術中分解、分模組思想很相近)。然後只把那些需要用到的指令和資料儲存在記憶體中,而把其餘的指令和資料儲存在記憶體外。關鍵是需要程式設計師手動來分塊。

這種技術有什麼問題呢?
這種技術必須由程式設計師手工把一個大的程式劃分為若干個小的功能模組,並確定各個模組之間的呼叫關係。手工做這種事情很費時費力,使得程式設計複雜度增加。但是,程式設計師總是愛“偷懶”的,於是,人們去尋找更好的方案。

這個方案就是虛擬記憶體技術,它的基本思路:
程式執行程式的總大小可以超過實際可用的實體記憶體的大小。每個程式都可以有自己獨立的虛擬地址空間。然後通過CPU和MMU把虛擬記憶體地址轉換為實際實體地址。

這個就相當於在實體記憶體和程式之間增加了一箇中間層,虛擬記憶體。
虛擬儲存也可以看作是對記憶體的一種抽象。而且這種抽象帶來諸多好處:

  1. 它將記憶體看成是一個儲存在磁碟上的地址空間的快取記憶體,在記憶體中只保留了活動區域,可以根據需要在磁碟和記憶體間來回傳送資料,高效使用記憶體。
  2. 它為每個程式提供了一致的地址空間,簡化了儲存的管理。
  3. 對程式起到保護作用,不被其他程式地址空間破壞,因為每個程式的地址空間都是相互獨立。

(程式:靜態的程式;程式:動態的,可以看作是程式的一個例項)

壞處:就是複雜度進一步增加,這也是必然的。不過相比帶來的好處,複雜度的增加還是可以接受,並克服。

Linux中對程式的處理抽象成了一個結構體 task_struct,我前面文章有對這個結構體的介紹。下面就看看程式的記憶體。

1.2 程式的記憶體

程式記憶體在linux(32位)中的佈局:

來自:https://manybutfinite.com/post/anatomy-of-a-program-in-memory/

最高位的1GB是linux核心空間,使用者程式碼不能寫,否則觸發段錯誤。下面的3GB是程式使用的記憶體。

Kernel space:linux核心空間記憶體
Stack:程式棧空間,程式執行時使用。它向下增長,系統自動管理
Memory Mapping Segment:記憶體對映區,通過mmap系統呼叫,將檔案對映到程式的地址空間,或者匿名對映。
Heap:堆空間。這個就是程式裡動態分配的空間。linux下使用malloc呼叫擴充套件(用brk/sbrk擴充套件記憶體空間),free函式釋放(也就是縮減記憶體空間)
BSS段:包含未初始化的靜態變數和全域性變數
Data段:程式碼裡已初始化的靜態變數、全域性變數
Text段:程式碼段,程式的可執行檔案

二、記憶體管理中的一些常見問題

1、未能釋放已經不再使用的記憶體 - 記憶體洩漏
2、指向不可用的記憶體指標 - 野指標
3、指標所指向的物件已經被回收了,但是指向該物件的指標仍舊指向已經回收的記憶體地址 - 懸掛指標
4、分配或釋放記憶體太快或者太慢
5、分配記憶體大小不合理,造成記憶體碎片問題
6、記憶體碎片問題

三、TCMalloc

可以檢視前面的文章 TCMalloc記憶體分配簡析,TCMalloc記憶體分配器的原理和golang記憶體分配器原理相近,所以理解了TCMalloc,golang記憶體分配原理也就理解大半,不過golang對它也有一些改動。

四、golang記憶體

4.1 golang怎麼解決常見記憶體問題

golang是怎麼解決 的記憶體管理中的常見問題的呢?

針對上面的1、2、3 這三種問題,golang使用自動垃圾回收機制,一般情況下,都不使用指標運算(要運算用unsafe包),很少的指標使用。當然,記憶體洩漏問題不能完全根除,但是可以解決一大部分問題。

針對下面的4、5、6 這三種問題,golang採用了多級快取,預分配的方法,來加快記憶體分配和釋放回收,儘量減少記憶體碎片。詳見 TCMalloc記憶體分配簡析

4.2 為什麼要重新寫一個記憶體分配器

核心已經有一個malloc的記憶體分配器,為什麼還有重寫一個記憶體分配器?

可以看到,malloc是一個很悠久的記憶體分配器,但是隨著時代的發展,多核多執行緒已經普及,為了更好的應用多執行緒,提高程式效率,以及改進記憶體碎片,所以重新寫了一個記憶體分配器。從這裡 TCMalloc記憶體分配簡析 可以看出TCMaloc的優點,它將記憶體劃分為多級別,減少鎖的開銷。而且每個執行緒的快取又分開了多個小的物件,以減少記憶體碎片。等等優化改進。

所以go記憶體分配也繼承了這些優點。go還有一個原因,那就是go還有GC,需要配合記憶體的垃圾回收。

4.3 記憶體管理到底管理哪個區域

從上面的程式記憶體佈局圖,可以看出一個程式的記憶體劃分了好多不同的區域,而記憶體管理主要管理的就是Stack和Heap,其中Stack (棧)區主要由編譯器和系統管理,程式語言主要管理Heap(堆)。而且這裡的程式記憶體指的是虛擬記憶體。

4.4 golang記憶體中的概念

golang記憶體分配的基本思想來自TCMalloc,所以go記憶體分配中的幾個概念與TCMalloc很相似,可以看看TCMalloc 中的概念

mspan

mspan跟tcmalloc中的span相似,它是golang記憶體管理中的基本單位,也是由頁組成的,每個頁大小為8KB,與tcmalloc中span組成的預設基本記憶體單位頁大小相同。mspan裡面按照8*2n大小(8b,16b,32b .... ),每一個mspan又分為多個object。
就連名字也很像,mspan中的m應該是memory的第一個字母。

mcache

mcache跟tcmalloc中的ThreadCache相似,ThreadCache為每個執行緒的cache,同理,mcache可以為golang中每個Processor提供記憶體cache使用,每一個mcache的組成單位也是mspan。

mcentral

mcentral跟tcmalloc中的CentralCache相似,當mcache中空間不夠用,可以向mcentral申請記憶體。可以理解為mcentral為mcache的一個“快取庫”,供mcaceh使用。它的記憶體組成單位也是mspan。
mcentral裡有兩個雙向連結串列,一個連結串列表示還有空閒的mspan待分配,一個表示連結串列裡的mspan都被分配了。

mheap

mheap跟tcmalloc中的PageHeap相似,負責大記憶體的分配。當mcentral記憶體不夠時,可以向mheap申請。那mheap沒有記憶體資源呢?跟tcmalloc一樣,向OS作業系統申請。
還有,大於32KB的記憶體,也是直接向mheap申請。

總結

golang記憶體分配幾個相關概念,用圖來總結一下:

後面再進一步分析golang的記憶體分配原理。

五、參考

相關文章