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

一、Linux系統記憶體

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

1.1 虛擬記憶體技術

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

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

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

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

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

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

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

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

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

1.2 進程的記憶體

進程記憶體在linux(32位)中的布局:

來自://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的記憶體分配原理。

五、參考