一般程序的內存分配
在講Golang的內存分配之前,讓我們先來看看一般程序的內存分布情況:
以上是程序內存的邏輯分類情況。
我們再來看看一般程序的內存的真實(真實邏輯)圖:
Go的內存分配核心思想
Go是內置運行時的編程語言(runtime),像這種內置運行時的編程語言通常會拋棄傳統的內存分配方式,改為自己管理。這樣可以完成類似預分配、內存池等操作,以避開系統調用帶來的性能問題,防止每次分配內存都需要系統調用。
Go的內存分配的核心思想可以分為以下幾點:
- 每次從操作系統申請一大塊兒的內存,由Go來對這塊兒內存做分配,減少系統調用
- 內存分配算法采用Google的
TCMalloc算法。算法比較復雜,究其原理可自行查閱。其核心思想就是把內存切分的非常的細小,分為多級管理,以降低鎖的粒度。 - 回收對象內存時,並沒有將其真正釋放掉,只是放回預先分配的大塊內存中,以便復用。只有內存閑置過多的時候,才會嘗試歸還部分內存給操作系統,降低整體開銷
Go的內存結構
Go在程序啟動的時候,會分配一塊連續的內存(虛擬內存)。整體如下:
圖中span和bitmap的大小會隨着heap的改變而改變
arena
arena區域就是我們通常所說的heap。
heap中按照管理和使用兩個維度可認為存在兩類“東西”:
一類是從管理分配角度,由多個連續的頁(page)組成的大塊內存:
另一類是從使用角度出發,就是平時咱們所了解的:heap中存在很多"對象":
spans
spans區域,可以認為是用於上面所說的管理分配arena(即heap)的區域。
此區域存放了mspan的指針,mspan是啥后面會講。
spans區域用於表示arena區中的某一頁(page)屬於哪個mspan。
mspan可以說是go內存管理的最基本單元,但是內存的使用最終還是要落腳到“對象”上。mspan和對象是什么關系呢?
其實“對象”肯定也放到page中,畢竟page是內存存儲的基本單元。
我們拋開問題不看,先看看一般情況下的對象和內存的分配是如何的:如下圖
假如再分配“p4”的時候,是不是內存不足沒法分配了?是不是有很多碎片?
這種一般的分配情況會出現內存碎片的情況,go是如何解決的呢?
可以歸結為四個字:按需分配。go將內存塊分為大小不同的67種,然后再把這67種大內存塊,逐個分為小塊(可以近似理解為大小不同的相當於page)稱之為span(連續的page),在go語言中就是上文提及的mspan。
對象分配的時候,根據對象的大小選擇大小相近的
span,這樣,碎片問題就解決了。
67中不同大小的span代碼注釋如下(目前版本1.11):
// class bytes/obj bytes/span objects tail waste max waste // 1 8 8192 1024 0 87.50% // 2 16 8192 512 0 43.75% // 3 32 8192 256 0 46.88% // 4 48 8192 170 32 31.52% // 5 64 8192 128 0 23.44% // 6 80 8192 102 32 19.07% // 7 96 8192 85 32 15.95% // 8 112 8192 73 16 13.56% // 9 128 8192 64 0 11.72% // 10 144 8192 56 128 11.82% // 11 160 8192 51 32 9.73% // 12 176 8192 46 96 9.59% // 13 192 8192 42 128 9.25% // 14 208 8192 39 80 8.12% // 15 224 8192 36 128 8.15% // 16 240 8192 34 32 6.62% // 17 256 8192 32 0 5.86% // 18 288 8192 28 128 12.16% // 19 320 8192 25 192 11.80% // 20 352 8192 23 96 9.88% // 21 384 8192 21 128 9.51% // 22 416 8192 19 288 10.71% // 23 448 8192 18 128 8.37% // 24 480 8192 17 32 6.82% // 25 512 8192 16 0 6.05% // 26 576 8192 14 128 12.33% // 27 640 8192 12 512 15.48% // 28 704 8192 11 448 13.93% // 29 768 8192 10 512 13.94% // 30 896 8192 9 128 15.52% // 31 1024 8192 8 0 12.40% // 32 1152 8192 7 128 12.41% // 33 1280 8192 6 512 15.55% // 34 1408 16384 11 896 14.00% // 35 1536 8192 5 512 14.00% // 36 1792 16384 9 256 15.57% // 37 2048 8192 4 0 12.45% // 38 2304 16384 7 256 12.46% // 39 2688 8192 3 128 15.59% // 40 3072 24576 8 0 12.47% // 41 3200 16384 5 384 6.22% // 42 3456 24576 7 384 8.83% // 43 4096 8192 2 0 15.60% // 44 4864 24576 5 256 16.65% // 45 5376 16384 3 256 10.92% // 46 6144 24576 4 0 12.48% // 47 6528 32768 5 128 6.23% // 48 6784 40960 6 256 4.36% // 49 6912 49152 7 768 3.37% // 50 8192 8192 1 0 15.61% // 51 9472 57344 6 512 14.28% // 52 9728 49152 5 512 3.64% // 53 10240 40960 4 0 4.99% // 54 10880 32768 3 128 6.24% // 55 12288 24576 2 0 11.45% // 56 13568 40960 3 256 9.99% // 57 14336 57344 4 0 5.35% // 58 16384 16384 1 0 12.49% // 59 18432 73728 4 0 11.11% // 60 19072 57344 3 128 3.57% // 61 20480 40960 2 0 6.87% // 62 21760 65536 3 256 6.25% // 63 24576 24576 1 0 11.45% // 64 27264 81920 3 128 10.00% // 65 28672 57344 2 0 4.91% // 66 32768 32768 1 0 12.50%
說說每列代表的含義:
- class: class ID,每個span結構中都有一個class ID, 表示該span可處理的對象類型
- bytes/obj:該class代表對象的字節數
- bytes/span:每個span占用堆的字節數,也即頁數*頁大小
- objects: 每個span可分配的對象個數,也即(bytes/spans)/(bytes/obj)
- waste bytes: 每個span產生的內存碎片,也即(bytes/spans)%(bytes/obj)
閱讀方式如下:
以類型(class)為1的span為例,span中的元素大小是8 byte, span本身占1頁也就是8K, 一共可以保存1024個對象。
細心的同學可能會發現代碼中一共有66種,還有一種特殊的span:
即對於大於32k的對象出現時,會直接從heap分配一個特殊的span,這個特殊的span的類型(class)是0, 只包含了一個大對象, span的大小由對象的大小決定。
bitmap
bitmap 有好幾種:Stack, data, and bss bitmaps,再就是這次要說的heap bitmaps。
在此bitmap的做作用是標記標記arena(即heap)中的對象。一是的標記對應地址中是否存在對象,另外是標記此對象是否被gc標記過。一個功能一個bit位,所以,heap bitmaps用兩個bit位。
bitmap區域中的一個byte對應arena區域的四個指針大小的內存的結構如下:
bitmap的地址是由高地址向低地址增長的。
宏觀的圖為:
bitmap 主要的作用還是服務於GC。
arena中包含基本的管理單元和程序運行時候生成的對象或實體,這兩部分分別被spans和bitmap這兩塊非heap區域的內存所對應着。
邏輯圖如下:
spans和bitmap都會根據arena的動態變化而動態調整大小。
內存管理組件
go的內存管理組件主要有:mspan、mcache、mcentral和mheap
-
mspan為內存管理的基礎單元,直接存儲數據的地方。 -
mcache:每個運行期的goroutine都會綁定的一個mcache(具體來講是綁定的GMP並發模型中的P,所以可以無鎖分配mspan,后續還會說到),mcache會分配goroutine運行中所需要的內存空間(即mspan)。 -
mcentral為所有mcache切分好后備的mspan -
mheap代表Go程序持有的所有堆空間。還會管理閑置的span,需要時向操作系統申請新內存。
mspan
有人會問:mspan結構體存放在哪兒?其實,mspan結構本身的內存是從系統分配的,在此不做過多討論。
mspan在上文講
spans的時候具體講過,就是方便根據對象大小來分配使用的內存塊,一共有67種類型;最主要解決的是內存碎片問題,減少了內存碎片,提高了內存使用率。
mspan是雙向鏈表,其中主要的屬性如下圖所示:
mspan是go中內存管理的基本單元,在上文spans中其實已經做了詳細的解說,在此就不在贅述了。
mcache
為了避免多線程申請內存時不斷的加鎖,goroutine為每個線程分配了span內存塊的緩存,這個緩存即是mcache,每個goroutine都會綁定的一個mcache,各個goroutine申請內存時不存在鎖競爭的情況。
如何做到的?
在講之前,請先回顧一下Go的並發調度模型,如果你還不了解,請看我這篇文章 https://mp.weixin.qq.com/s/74hbRTQ2TjdH5G9F2of4_g
然后請看下圖:
大體上就是上圖這個樣子了。注意看我們的mcache在哪兒呢?就在P上!
知道為什么沒有鎖競爭了吧,因為運行期間一個goroutine只能和一個P關聯,而mcache就在P上,所以,不可能有鎖的競爭。
我們再來看看mcache具體的結構:
mcache中的span鏈表分為兩組,一組是包含指針類型的對象,另一組是不包含指針類型的對象。為什么分開呢?
主要是方便GC,在進行垃圾回收的時候,對於不包含指針的對象列表無需進一步掃描是否引用其他活躍的對象(如果對go的gc不是很了解,請看我這篇文章 https://mp.weixin.qq.com/s/_h0-8hma5y_FHKBeFuOOyw)。
對於 <=32k的對象,將直接通過mcache分配。
在此,我覺的有必要說一下go中對象按照的大小維度的分類。
分為三類:
- tinny allocations (size < 16 bytes,no pointers)
- small allocations (16 bytes < size <= 32k)
- large allocations (size > 32k)
前兩類:tiny allocations和small allocations是直接通過mcache來分配的。
對於tiny allocations的分配,有一個微型分配器tiny allocator來分配,分配的對象都是不包含指針的,例如一些小的字符串和不包含指針的獨立的逃逸變量等。
small allocations的分配,就是mcache根據對象的大小來找自身存在的大小相匹配mspan來分配。
當mcach沒有可用空間時,會從mcentral的 mspans 列表獲取一個新的所需大小規格的mspan。
mcentral
為所有mcache提供切分好的mspan。
每個mcentral保存一種特定類型的全局mspan列表,包括已分配出去的和未分配出去的。
還記得mspan的67種類型嗎?有多少種類型的mspan就有多少個mcentral。
每個mcentral都會包含兩個mspan的列表:
- 沒有空閑對象或
mspan已經被mcache緩存的mspan列表(empty mspanList) - 有空閑對象的
mspan列表(empty mspanList)
由於mspan是全局的,會被所有的mcache訪問,所以會出現並發性問題,因而mcentral會存在一個鎖。
單個的mcentral結構如下:
假如需要分配內存時,mcentral沒有空閑的mspan列表了,此時需要向mheap去獲取。
mheap
mheap可以認為是Go程序持有的整個堆空間,mheap全局唯一,可以認為是個全局變量。
其結構如下:
mheap包含了除了上文中講的mcache之外的一切,mcache是存在於Go的GMP調度模型的P中的,上文中已經講過了,關於GMP並發模型,可以參考我的文章 https://mp.weixin.qq.com/s/74hbRTQ2TjdH5G9F2of4_g。
仔細觀察,可以發現mheap中也存在一個鎖lock。這個lock是作用是什么呢?
我們知道,大於32K的對象被定義為大對象,直接通過mheap 分配。這些大對象的申請是由mcache發出的,而mcache在P上,程序運行的時候往往會存在多個P,因此,這個內存申請是並發的;所以為了保證線程安全,必須有一個全局鎖。
假如需要分配的內存時,mheap中也沒有了,則向操作系統申請一系列新的頁(最小 1MB)。
Go內存分配流程總結
對象分三種:
- 微小對象,size < 16B
- 一般小對象, 16 bytes < size <= 32k
- 大對象 size > 32k
分配方式分三種:
- tinny allocations (size < 16 bytes,no pointers) 微型分配器分配。
- small allocations ( size <= 32k) 正常分配;首先通過計算使用的大小規格,然后使用 mcache 中對應大小規格的塊分配
- large allocations (size > 32k) 大對象分配;直接通過
mheap分配。這些大對象的申請是以一個全局鎖為代價的,因此任何給定的時間點只能同時供一個 P 申請。
對象分配:
- size范圍在在( size < 16B),不包含指針的對象。
mcache上的微型分配器分配 - size范圍在(0 < size < 16B), 包含指針的對象:正常分配
- size范圍在(16B < size <= 32KB), : 正常分配
- size范圍在( size > 32KB) : 大對象分配
分配順序:
- 首先通過計算使用的大小規格。
- 然后使用
mcache中對應大小規格的塊分配。 - 如果
mcentral中沒有可用的塊,則向mheap申請,並根據算法找到最合適的mspan。 - 如果申請到的
mspan超出申請大小,將會根據需求進行切分,以返回用戶所需的頁數。剩余的頁構成一個新的 mspan 放回 mheap 的空閑列表。 - 如果 mheap 中沒有可用 span,則向操作系統申請一系列新的頁(最小 1MB)。
Go的內存管理是非常復雜的,且每個版本都有細微的變化,在此,只講了些最容易宏觀掌握的東西,希望大家多多提意見,如有什么問題,請及時與我溝通,以下是聯系方式:
參考文獻:
- 程序在內存中的分布 https://www.cnblogs.com/Lynn-Zhang/p/5449199.html
- 從內存分配開始 https://mp.weixin.qq.com/s/EyWKFRu1xryoHY386QUcuA
- 譯文:Go 內存分配器可視化指南 https://www.linuxzen.com/go-memory-allocator-visual-guide.html
- 圖解Go語言內存分配 https://juejin.im/post/5c888a79e51d456ed11955a8
- Golang源碼探索(三) GC的實現原理 https://www.cnblogs.com/zkweb/p/7880099.html
- 簡單易懂的 Go 內存分配原理解讀 https://yq.aliyun.com/articles/652551
- 雨痕<<Go源碼解析>>
- go內存分配(英文) https://andrestc.com/post/go-memory-allocation-pt1/
作者:RyuGou
鏈接:https://www.jianshu.com/p/2904efc7f1a8
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處
